From 317736d726d66872942347ec1b267fa5887966ed Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 2 Aug 2018 15:03:50 -0500 Subject: [PATCH 01/30] swarm: swap implementation as devp2p2 protocol --- swarm/swap/api.go | 62 ++++++ swarm/swap/protocol.go | 177 ++++++++++++++++ swarm/swap/swap.go | 433 ++++++++++++++++++++++++++++++++++++++++ swarm/swap/swap_test.go | 275 +++++++++++++++++++++++++ swarm/swarm.go | 14 ++ 5 files changed, 961 insertions(+) create mode 100644 swarm/swap/api.go create mode 100644 swarm/swap/protocol.go create mode 100644 swarm/swap/swap.go create mode 100644 swarm/swap/swap_test.go diff --git a/swarm/swap/api.go b/swarm/swap/api.go new file mode 100644 index 0000000000..2de11d9efa --- /dev/null +++ b/swarm/swap/api.go @@ -0,0 +1,62 @@ +// Copyright 2018 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 + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// Wrapper for receiving pss messages when using the pss API +// providing access to sender of message +type APIMsg struct { + Msg hexutil.Bytes +} + +// Additional public methods accessible through API for pss +type API struct { + *Swap +} + +type SwapMetrics struct { +} + +type Cheque struct { +} + +func NewAPI(swap *Swap) *API { + return &API{Swap: swap} +} + +func (swapapi *API) Balance(ctx context.Context) (balance int, err error) { + balance = 0 + err = nil + return +} + +func (swapapi *API) GetSwapMetrics() (*SwapMetrics, error) { + return nil, nil +} + +func (swapapi *API) IssueCheque(recipient *common.Address) (*Cheque, error) { + return nil, nil +} + +func (swapapi *API) RedeemCheque(cheque *Cheque) { +} diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go new file mode 100644 index 0000000000..63acbbe929 --- /dev/null +++ b/swarm/swap/protocol.go @@ -0,0 +1,177 @@ +// Copyright 2018 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 . + +// +build !nopssprotocol + +package swap + +import ( + "sync" + "time" + + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/protocols" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + IsActiveProtocol = true +) + +// Convenience wrapper for devp2p protocol messages for transport over pss +type ProtocolMsg struct { + Code uint64 + Size uint32 + Payload []byte + ReceivedAt time.Time +} + +// Creates a ProtocolMsg +func NewProtocolMsg(code uint64, msg interface{}) ([]byte, error) { + + rlpdata, err := rlp.EncodeToBytes(msg) + if err != nil { + return nil, err + } + + // TODO verify that nested structs cannot be used in rlp + smsg := &ProtocolMsg{ + Code: code, + Size: uint32(len(rlpdata)), + Payload: rlpdata, + } + + return rlp.EncodeToBytes(smsg) +} + +// Protocol options to be passed to a new Protocol instance +// +// The parameters specify which encryption schemes to allow +type ProtocolParams struct { +} + +// Convenience object for emulation devp2p over pss +type Protocol struct { + proto *p2p.Protocol + spec *protocols.Spec + RWPoolMu sync.Mutex +} + +/* +// Activates devp2p emulation over a specific pss topic +// +// One or both encryption schemes must be specified. If +// only one is specified, the protocol will not be valid +// for the other, and will make the message handler +// return errors +func RegisterProtocol(ps *Pss, topic *Topic, spec *protocols.Spec, targetprotocol *p2p.Protocol, options *ProtocolParams) (*Protocol, error) { + if !options.Asymmetric && !options.Symmetric { + return nil, fmt.Errorf("specify at least one of asymmetric or symmetric messaging mode") + } + pp := &Protocol{ + Pss: ps, + proto: targetprotocol, + topic: topic, + spec: spec, + pubKeyRWPool: make(map[string]p2p.MsgReadWriter), + symKeyRWPool: make(map[string]p2p.MsgReadWriter), + Asymmetric: options.Asymmetric, + Symmetric: options.Symmetric, + } + return pp, nil +} + +*/ +// Generic handler for incoming messages over devp2p emulation +// +// To be passed to pss.Register() +// +// Will run the protocol on a new incoming peer, provided that +// the encryption key of the message has a match in the internal +// pss keypool +// +// Fails if protocol is not valid for the message encryption scheme, +// if adding a new peer fails, or if the message is not a serialized +// p2p.Msg (which it always will be if it is sent from this object). +func (p *Protocol) Handle(msg []byte, peer *p2p.Peer, asymmetric bool, keyid string) error { + return nil +} + +/* +// Runs an emulated pss Protocol on the specified peer, +// linked to a specific topic +// `key` and `asymmetric` specifies what encryption key +// to link the peer to. +// The key must exist in the pss store prior to adding the peer. +func (p *Protocol) AddPeer(peer *p2p.Peer, topic Topic, asymmetric bool, key string) (p2p.MsgReadWriter, error) { + rw := &PssReadWriter{ + Pss: p.Pss, + rw: make(chan p2p.Msg), + spec: p.spec, + topic: p.topic, + key: key, + } + if asymmetric { + rw.sendFunc = p.Pss.SendAsym + } else { + rw.sendFunc = p.Pss.SendSym + } + if asymmetric { + p.Pss.pubKeyPoolMu.Lock() + if _, ok := p.Pss.pubKeyPool[key]; !ok { + return nil, fmt.Errorf("asym key does not exist: %s", key) + } + p.Pss.pubKeyPoolMu.Unlock() + p.RWPoolMu.Lock() + p.pubKeyRWPool[key] = rw + p.RWPoolMu.Unlock() + } else { + p.Pss.symKeyPoolMu.Lock() + if _, ok := p.Pss.symKeyPool[key]; !ok { + return nil, fmt.Errorf("symkey does not exist: %s", key) + } + p.Pss.symKeyPoolMu.Unlock() + p.RWPoolMu.Lock() + p.symKeyRWPool[key] = rw + p.RWPoolMu.Unlock() + } + go func() { + err := p.proto.Run(peer, rw) + log.Warn(fmt.Sprintf("pss vprotocol quit on %v topic %v: %v", peer, topic, err)) + }() + return rw, nil +} + +func (p *Protocol) RemovePeer(asymmetric bool, key string) { + log.Debug("closing pss peer", "asym", asymmetric, "key", key) + p.RWPoolMu.Lock() + defer p.RWPoolMu.Unlock() + if asymmetric { + rw := p.pubKeyRWPool[key].(*PssReadWriter) + rw.closed = true + delete(p.pubKeyRWPool, key) + } else { + rw := p.symKeyRWPool[key].(*PssReadWriter) + rw.closed = true + delete(p.symKeyRWPool, key) + } +} + +// Uniform translation of protocol specifiers to topic +func ProtocolTopic(spec *protocols.Spec) Topic { + return BytesToTopic([]byte(fmt.Sprintf("%s:%d", spec.Name, spec.Version))) +} +*/ diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go new file mode 100644 index 0000000000..7b58331cbf --- /dev/null +++ b/swarm/swap/swap.go @@ -0,0 +1,433 @@ +// Copyright 2018 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 + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + "os" + "path/filepath" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/contracts/chequebook" + "github.com/ethereum/go-ethereum/contracts/chequebook/contract" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/protocols" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/swarm/log" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" +) + +const ( + defaultMaxMsgSize = 1024 * 1024 + swapProtocolName = "swap" + swapVersion = 1 +) + +var ( + autoCashInterval = 300 * time.Second // default interval for autocash + autoCashThreshold = big.NewInt(50000000000000) // threshold that triggers autocash (wei) + autoDepositInterval = 300 * time.Second // default interval for autocash + autoDepositThreshold = big.NewInt(50000000000000) // threshold that triggers autodeposit (wei) + autoDepositBuffer = big.NewInt(100000000000000) // buffer that is surplus for fork protection etc (wei) + buyAt = big.NewInt(20000000000) // maximum chunk price host is willing to pay (wei) + sellAt = big.NewInt(20000000000) // minimum chunk price host requires (wei) + payAt = 100 // threshold that triggers payment {request} (units) + dropAt = 10000 // threshold that triggers disconnect (units) +) + +const ( + chequebookDeployRetries = 5 + chequebookDeployDelay = 1 * time.Second // delay between retries +) + +// SwAP Swarm Accounting Protocol with +// Swift Automatic Payments +// a peer to peer micropayment system +type Swap struct { + lock sync.Mutex // mutex for balance access + balance int // units of chunk/retrieval request + local *Params // local peer's swap parameters + remote *Profile // remote peer's swap profile +} + +// Profile - public swap profile +// public parameters for SWAP, serializable config struct passed in handshake +type Profile struct { + BuyAt *big.Int // accepted max price for chunk + SellAt *big.Int // offered sale price for chunk + PayAt uint // threshold that triggers payment request + DropAt uint // threshold that triggers disconnect +} + +// Strategy encapsulates parameters relating to +// automatic deposit and automatic cashing +type Strategy struct { + AutoCashInterval time.Duration // default interval for autocash + AutoCashThreshold *big.Int // threshold that triggers autocash (wei) + AutoDepositInterval time.Duration // default interval for autocash + AutoDepositThreshold *big.Int // threshold that triggers autodeposit (wei) + AutoDepositBuffer *big.Int // buffer that is surplus for fork protection etc (wei) +} + +// SwapMsg encapsulates messages transported over pss. +type SwapMsg struct { + To []byte + Control []byte + Expire uint32 + Payload *whisper.Envelope +} + +// Params extends the public profile with private parameters relating to +// automatic deposit and automatic cashing +type Params struct { + *Profile + *Strategy +} + +// LocalProfile combines a PayProfile with *swap.Params +type LocalProfile struct { + *Params + *PayProfile +} + +// RemoteProfile combines a PayProfile with *swap.Profile +type RemoteProfile struct { + *Profile + *PayProfile +} + +// PayProfile is a container for relevant chequebook and beneficiary options +type PayProfile struct { + PublicKey string // check against signature of promise + Contract common.Address // address of chequebook contract + Beneficiary common.Address // recipient address for swarm sales revenue + privateKey *ecdsa.PrivateKey + publicKey *ecdsa.PublicKey + owner common.Address + chbook *chequebook.Chequebook + lock sync.RWMutex +} + +// New - swap constructor +func NewSwap(local *Params) (swap *Swap, err error) { + + swap = &Swap{ + local: local, + } + + //swap.SetParams(local) + + return +} + +// NewDefaultSwapParams create params with default values +func NewDefaultSwapParams() *LocalProfile { + return &LocalProfile{ + PayProfile: &PayProfile{}, + Params: &Params{ + Profile: &Profile{ + BuyAt: buyAt, + SellAt: sellAt, + PayAt: uint(payAt), + DropAt: uint(dropAt), + }, + Strategy: &Strategy{ + AutoCashInterval: autoCashInterval, + AutoCashThreshold: autoCashThreshold, + AutoDepositInterval: autoDepositInterval, + AutoDepositThreshold: autoDepositThreshold, + AutoDepositBuffer: autoDepositBuffer, + }, + }, + } +} + +// Init this can only finally be set after all config options (file, cmd line, env vars) +// have been evaluated +func (lp *LocalProfile) Init(contract common.Address, prvkey *ecdsa.PrivateKey) { + pubkey := &prvkey.PublicKey + + lp.PayProfile = &PayProfile{ + PublicKey: common.ToHex(crypto.FromECDSAPub(pubkey)), + Contract: contract, + Beneficiary: crypto.PubkeyToAddress(*pubkey), + privateKey: prvkey, + publicKey: pubkey, + owner: crypto.PubkeyToAddress(*pubkey), + } +} + +///////////////////////////////////////////////////////////////////// +// SECTION: node.Service interface +///////////////////////////////////////////////////////////////////// + +func (p *Swap) Start(srv *p2p.Server) error { + log.Debug("Started swap") + return nil +} + +func (p *Swap) Stop() error { + log.Info("swap shutting down") + return nil +} + +var swapSpec = &protocols.Spec{ + Name: swapProtocolName, + Version: swapVersion, + MaxMsgSize: defaultMaxMsgSize, + Messages: []interface{}{ + SwapMsg{}, + }, +} + +func (p *Swap) Protocols() []p2p.Protocol { + return []p2p.Protocol{ + { + Name: swapSpec.Name, + Version: swapSpec.Version, + Length: swapSpec.Length(), + Run: p.Run, + }, + } +} + +func (p *Swap) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { + pp := protocols.NewPeer(peer, rw, swapSpec) + /* + p.fwdPoolMu.Lock() + p.fwdPool[peer.Info().ID] = pp + p.fwdPoolMu.Unlock() + */ + return pp.Run(p.handleSwapMsg) +} + +func (p *Swap) APIs() []rpc.API { + apis := []rpc.API{ + { + Namespace: "swap", + Version: "1.0", + Service: NewAPI(p), + Public: true, + }, + } + return apis +} + +// Filters incoming messages for processing or forwarding. +// Check if address partially matches +// If yes, it CAN be for us, and we process it +// Only passes error to pss protocol handler if payload is not valid pssmsg +func (p *Swap) handleSwapMsg(ctx context.Context, msg interface{}) error { + return nil +} + +// Chequebook get's chequebook from the localProfile +func (lp *LocalProfile) Chequebook() *chequebook.Chequebook { + defer lp.lock.Unlock() + lp.lock.Lock() + return lp.chbook +} + +// PrivateKey accessor +func (lp *LocalProfile) PrivateKey() *ecdsa.PrivateKey { + return lp.privateKey +} + +// func (self *LocalProfile) PublicKey() *ecdsa.PublicKey { +// return self.publicKey +// } + +// SetKey set's private and public key on localProfile +func (lp *LocalProfile) SetKey(prvkey *ecdsa.PrivateKey) { + lp.privateKey = prvkey + lp.publicKey = &prvkey.PublicKey +} + +// SetChequebook wraps the chequebook initialiser and sets up autoDeposit to cover spending. +func (lp *LocalProfile) SetChequebook(ctx context.Context, backend chequebook.Backend, path string) error { + lp.lock.Lock() + swapContract := lp.Contract + lp.lock.Unlock() + + valid, err := chequebook.ValidateCode(ctx, backend, swapContract) + if err != nil { + return err + } else if valid { + return lp.newChequebookFromContract(path, backend) + } + return lp.deployChequebook(ctx, backend, path) +} + +// deployChequebook deploys the localProfile Chequebook +func (lp *LocalProfile) deployChequebook(ctx context.Context, backend chequebook.Backend, path string) error { + opts := bind.NewKeyedTransactor(lp.privateKey) + opts.Value = lp.AutoDepositBuffer + opts.Context = ctx + + log.Info(fmt.Sprintf("Deploying new chequebook (owner: %v)", opts.From.Hex())) + address, err := deployChequebookLoop(opts, backend) + if err != nil { + log.Error(fmt.Sprintf("unable to deploy new chequebook: %v", err)) + return err + } + log.Info(fmt.Sprintf("new chequebook deployed at %v (owner: %v)", address.Hex(), opts.From.Hex())) + + // need to save config at this point + lp.lock.Lock() + lp.Contract = address + err = lp.newChequebookFromContract(path, backend) + lp.lock.Unlock() + if err != nil { + log.Warn(fmt.Sprintf("error initialising cheque book (owner: %v): %v", opts.From.Hex(), err)) + } + return err +} + +// deployChequebookLoop repeatedly tries to deploy a chequebook. +func deployChequebookLoop(opts *bind.TransactOpts, backend chequebook.Backend) (addr common.Address, err error) { + var tx *types.Transaction + for try := 0; try < chequebookDeployRetries; try++ { + if try > 0 { + time.Sleep(chequebookDeployDelay) + } + if _, tx, _, err = contract.DeployChequebook(opts, backend); err != nil { + log.Warn(fmt.Sprintf("can't send chequebook deploy tx (try %d): %v", try, err)) + continue + } + if addr, err = bind.WaitDeployed(opts.Context, backend, tx); err != nil { + log.Warn(fmt.Sprintf("chequebook deploy error (try %d): %v", try, err)) + continue + } + return addr, nil + } + return addr, err +} + +// newChequebookFromContract - initialise the chequebook from a persisted json file or create a new one +// caller holds the lock +func (lp *LocalProfile) newChequebookFromContract(path string, backend chequebook.Backend) error { + hexkey := common.Bytes2Hex(lp.Contract.Bytes()) + err := os.MkdirAll(filepath.Join(path, "chequebooks"), os.ModePerm) + if err != nil { + return fmt.Errorf("unable to create directory for chequebooks: %v", err) + } + + chbookpath := filepath.Join(path, "chequebooks", hexkey+".json") + lp.chbook, err = chequebook.LoadChequebook(chbookpath, lp.privateKey, backend, true) + + if err != nil { + lp.chbook, err = chequebook.NewChequebook(chbookpath, lp.Contract, lp.privateKey, backend) + if err != nil { + log.Warn(fmt.Sprintf("unable to initialise chequebook (owner: %v): %v", lp.owner.Hex(), err)) + return fmt.Errorf("unable to initialise chequebook (owner: %v): %v", lp.owner.Hex(), err) + } + } + + lp.chbook.AutoDeposit(lp.AutoDepositInterval, lp.AutoDepositThreshold, lp.AutoDepositBuffer) + log.Info(fmt.Sprintf("auto deposit ON for %v -> %v: interval = %v, threshold = %v, buffer = %v)", crypto.PubkeyToAddress(*(lp.publicKey)).Hex()[:8], lp.Contract.Hex()[:8], lp.AutoDepositInterval, lp.AutoDepositThreshold, lp.AutoDepositBuffer)) + + return nil +} + +// Add (n) +// n > 0 called when promised/provided n units of service +// n < 0 called when used/requested n units of service +func (swap *Swap) Add(n int) error { + defer swap.lock.Unlock() + swap.lock.Lock() + swap.balance += n + if !swap.Sells && swap.balance > 0 { + log.Trace(fmt.Sprintf("<%v> remote peer cannot have debt (balance: %v)", swap.proto, swap.balance)) + swap.proto.Drop() + return fmt.Errorf("[SWAP] <%v> remote peer cannot have debt (balance: %v)", swap.proto, swap.balance) + } + if !swap.Buys && swap.balance < 0 { + log.Trace(fmt.Sprintf("<%v> we cannot have debt (balance: %v)", swap.proto, swap.balance)) + return fmt.Errorf("[SWAP] <%v> we cannot have debt (balance: %v)", swap.proto, swap.balance) + } + if swap.balance >= int(swap.local.DropAt) { + log.Trace(fmt.Sprintf("<%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", swap.proto, swap.balance, swap.local.DropAt)) + swap.proto.Drop() + return fmt.Errorf("[SWAP] <%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", swap.proto, swap.balance, swap.local.DropAt) + } else if swap.balance <= -int(swap.remote.PayAt) { + swap.send() + } + return nil +} + +// Balance accessor +func (swap *Swap) Balance() int { + defer swap.lock.Unlock() + swap.lock.Lock() + return swap.balance +} + +// send (units) is called when payment is due +// In case of insolvency no promise is issued and sent, safe against fraud +// No return value: no error = payment is opportunistic = hang in till dropped +func (swap *Swap) send() { + if swap.local.BuyAt != nil && swap.balance < 0 { + amount := big.NewInt(int64(-swap.balance)) + amount.Mul(amount, swap.remote.SellAt) + promise, err := swap.Out.Issue(amount) + if err != nil { + log.Warn(fmt.Sprintf("<%v> cannot issue cheque (amount: %v, channel: %v): %v", swap.proto, amount, swap.Out, err)) + } else { + log.Warn(fmt.Sprintf("<%v> cheque issued (amount: %v, channel: %v)", swap.proto, amount, swap.Out)) + swap.proto.Pay(-swap.balance, promise) + swap.balance = 0 + } + } +} + +// Receive (units, promise) is called by the protocol when a payment msg is received +// returns error if promise is invalid. +func (swap *Swap) Receive(units int, promise Promise) error { + if units <= 0 { + return fmt.Errorf("invalid units: %v <= 0", units) + } + + price := new(big.Int).SetInt64(int64(units)) + price.Mul(price, swap.local.SellAt) + + amount, err := swap.In.Receive(promise) + + if err != nil { + err = fmt.Errorf("invalid promise: %v", err) + } else if price.Cmp(amount) != 0 { + // verify amount = units * unit sale price + return fmt.Errorf("invalid amount: %v = %v * %v (units sent in msg * agreed sale unit price) != %v (signed in cheque)", price, units, swap.local.SellAt, amount) + } + if err != nil { + log.Trace(fmt.Sprintf("<%v> invalid promise (amount: %v, channel: %v): %v", swap.proto, amount, swap.In, err)) + return err + } + + // credit remote peer with units + swap.Add(-units) + log.Trace(fmt.Sprintf("<%v> received promise (amount: %v, channel: %v): %v", swap.proto, amount, swap.In, promise)) + + return nil +} diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go new file mode 100644 index 0000000000..289ba028be --- /dev/null +++ b/swarm/swap/swap_test.go @@ -0,0 +1,275 @@ +// Copyright 2018 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 + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" + colorable "github.com/mattn/go-colorable" +) + +var ( + p2pPort = 30100 + ipcpath = ".swarm.ipc" + datadirPrefix = ".data_" + stackW = &sync.WaitGroup{} + loglevel = flag.Int("loglevel", 2, "verbosity of logs") +) + +func init() { + flag.Parse() + + log.PrintOrigins(true) + log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) +} + +func newServiceNode(port int, httpport int, wsport int, modules ...string) (*node.Node, error) { + cfg := &node.DefaultConfig + cfg.P2P.ListenAddr = fmt.Sprintf(":%d", port) + cfg.P2P.EnableMsgEvents = true + cfg.P2P.NoDiscovery = true + cfg.IPCPath = ipcpath + cfg.DataDir = fmt.Sprintf("%s%d", datadirPrefix, port) + if httpport > 0 { + cfg.HTTPHost = node.DefaultHTTPHost + cfg.HTTPPort = httpport + } + if wsport > 0 { + cfg.WSHost = node.DefaultWSHost + cfg.WSPort = wsport + cfg.WSOrigins = []string{"*"} + for i := 0; i < len(modules); i++ { + cfg.WSModules = append(cfg.WSModules, modules[i]) + } + } + stack, err := node.New(cfg) + if err != nil { + return nil, fmt.Errorf("ServiceNode create fail: %v", err) + } + return stack, nil +} + +func TestBasicSwap(t *testing.T) { + + // create the two nodes + stack_one, err := newServiceNode(p2pPort, 0, 0) + if err != nil { + log.Crit("Create servicenode #1 fail", "err", err) + } + stack_two, err := newServiceNode(p2pPort+1, 0, 0) + if err != nil { + log.Crit("Create servicenode #2 fail", "err", err) + } + + instance, err := NewSwap(NewDefaultSwapParams().Params) + if err != nil { + t.Fatal("Couldn't create Swap instance") + } + // wrapper function for servicenode to start the service + swapsvc := func(ctx *node.ServiceContext) (node.Service, error) { + return &API{ + Swap: instance, + }, nil + } + + // register adds the service to the services the servicenode starts when started + err = stack_one.Register(swapsvc) + if err != nil { + log.Crit("Register service in servicenode #1 fail", "err", err) + } + err = stack_two.Register(swapsvc) + if err != nil { + log.Crit("Register service in servicenode #2 fail", "err", err) + } + + // start the nodes + err = stack_one.Start() + if err != nil { + log.Crit("servicenode #1 start failed", "err", err) + } + err = stack_two.Start() + if err != nil { + log.Crit("servicenode #2 start failed", "err", err) + } + + // connect to the servicenode RPCs + rpcclient_one, err := rpc.Dial(filepath.Join(stack_one.DataDir(), ipcpath)) + if err != nil { + log.Crit("connect to servicenode #1 IPC fail", "err", err) + } + defer os.RemoveAll(stack_one.DataDir()) + + rpcclient_two, err := rpc.Dial(filepath.Join(stack_two.DataDir(), ipcpath)) + if err != nil { + log.Crit("connect to servicenode #2 IPC fail", "err", err) + } + defer os.RemoveAll(stack_two.DataDir()) + + // display that the initial pong counts are 0 + var balance int + err = rpcclient_one.Call(&balance, "swap_balance") + if err != nil { + log.Crit("servicenode #1 pongcount RPC failed", "err", err) + } + log.Info("servicenode #1 before ping", "balance-1", balance) + + err = rpcclient_two.Call(&balance, "swap_balance") + if err != nil { + log.Crit("servicenode #2 pongcount RPC failed", "err", err) + } + log.Info("servicenode #2 before ping", "balance-2", balance) + + /* + // get the server instances + srv_one := stack_one.Server() + srv_two := stack_two.Server() + + // subscribe to peerevents + eventOneC := make(chan *p2p.PeerEvent) + sub_one := srv_one.SubscribeEvents(eventOneC) + + eventTwoC := make(chan *p2p.PeerEvent) + sub_two := srv_two.SubscribeEvents(eventTwoC) + + // connect the nodes + p2pnode_two := srv_two.Self() + srv_one.AddPeer(p2pnode_two) + + // fork and do the pinging + stackW.Add(2) + pingmax_one := 4 + pingmax_two := 2 + + go func() { + + // when we get the add event, we know we are connected + ev := <-eventOneC + if ev.Type != "add" { + log.Error("server #1 expected peer add", "eventtype", ev.Type) + stackW.Done() + return + } + log.Debug("server #1 connected", "peer", ev.Peer) + + // send the pings + for i := 0; i < pingmax_one; i++ { + err := rpcclient_one.Call(nil, "foo_ping", ev.Peer) + if err != nil { + log.Error("server #1 RPC ping fail", "err", err) + stackW.Done() + break + } + } + + // wait for all msgrecv events + // pings we receive, and pongs we expect from pings we sent + for i := 0; i < pingmax_two+pingmax_one; { + ev := <-eventOneC + log.Warn("msg", "type", ev.Type, "i", i) + if ev.Type == "msgrecv" { + i++ + } + } + + stackW.Done() + }() + + // mirrors the previous go func + go func() { + ev := <-eventTwoC + if ev.Type != "add" { + log.Error("expected peer add", "eventtype", ev.Type) + stackW.Done() + return + } + log.Debug("server #2 connected", "peer", ev.Peer) + for i := 0; i < pingmax_two; i++ { + err := rpcclient_two.Call(nil, "foo_ping", ev.Peer) + if err != nil { + log.Error("server #2 RPC ping fail", "err", err) + stackW.Done() + break + } + } + + for i := 0; i < pingmax_one+pingmax_two; { + ev := <-eventTwoC + if ev.Type == "msgrecv" { + log.Warn("msg", "type", ev.Type, "i", i) + i++ + } + } + + stackW.Done() + }() + + // wait for the two ping pong exchanges to finish + stackW.Wait() + + // tell the API to shut down + // this will disconnect the peers and close the channels connecting API and protocol + err = rpcclient_one.Call(nil, "foo_quit", srv_two.Self().ID) + if err != nil { + log.Error("server #1 RPC quit fail", "err", err) + } + err = rpcclient_two.Call(nil, "foo_quit", srv_one.Self().ID) + if err != nil { + log.Error("server #2 RPC quit fail", "err", err) + } + + // disconnect will generate drop events + for { + ev := <-eventOneC + if ev.Type == "drop" { + break + } + } + for { + ev := <-eventTwoC + if ev.Type == "drop" { + break + } + } + + // proudly inspect the results + err = rpcclient_one.Call(&count, "foo_pongCount") + if err != nil { + log.Crit("servicenode #1 pongcount RPC failed", "err", err) + } + log.Info("servicenode #1 after ping", "pongcount", count) + + err = rpcclient_two.Call(&count, "foo_pongCount") + if err != nil { + log.Crit("servicenode #2 pongcount RPC failed", "err", err) + } + log.Info("servicenode #2 after ping", "pongcount", count) + + // bring down the servicenodes + sub_one.Unsubscribe() + sub_two.Unsubscribe() + stack_one.Stop() + stack_two.Stop() + */ +} diff --git a/swarm/swarm.go b/swarm/swarm.go index aea0989a1e..e2585c6de3 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -51,6 +51,8 @@ import ( "github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/swarm/storage/feed" "github.com/ethereum/go-ethereum/swarm/storage/mock" + "github.com/ethereum/go-ethereum/swarm/storage/mru" + "github.com/ethereum/go-ethereum/swarm/swap" "github.com/ethereum/go-ethereum/swarm/tracing" ) @@ -78,6 +80,7 @@ type Swarm struct { netStore *storage.NetStore sfs *fuse.SwarmFS // need this to cleanup all the active mounts on node exit ps *pss.Pss + swap *swap.Swap tracerClose io.Closer } @@ -206,6 +209,10 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e self.bzz = network.NewBzz(bzzconfig, to, stateStore, stream.Spec, self.streamer.Run) + self.swap, err = swap.NewSwap(config.Swap.Params) + if err != nil { + return nil, err + } // Pss = postal service over swarm (devp2p over bzz) self.ps, err = pss.NewPss(to, config.Pss) if err != nil { @@ -361,6 +368,13 @@ func (self *Swarm) Start(srv *p2p.Server) error { } log.Info("Swarm network started", "bzzaddr", fmt.Sprintf("%x", self.bzz.Hive.BaseAddr())) + err = self.swap.Start(srv) + if err != nil { + log.Error("swap failed", "err", err) + return err + } + log.Debug("Swap accounting initialized") + if self.ps != nil { self.ps.Start(srv) } From c89cc174532c21bc7475dbf0ce8679cd00e11b6f Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 9 Aug 2018 12:34:36 -0500 Subject: [PATCH 02/30] swarm/swap: initial version --- swarm/network/stream/delivery.go | 8 + swarm/network/stream/stream.go | 20 ++- swarm/swap/api.go | 6 +- swarm/swap/protocol.go | 274 +++++++++++++++++-------------- swarm/swap/swap.go | 155 ++++++++--------- swarm/swap/swap_test.go | 9 +- swarm/swarm.go | 24 +-- 7 files changed, 267 insertions(+), 229 deletions(-) diff --git a/swarm/network/stream/delivery.go b/swarm/network/stream/delivery.go index 0429c4dffc..29e6a2b938 100644 --- a/swarm/network/stream/delivery.go +++ b/swarm/network/stream/delivery.go @@ -19,6 +19,8 @@ package stream import ( "context" "errors" + "math/big" + "time" "fmt" @@ -28,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/swarm/network" "github.com/ethereum/go-ethereum/swarm/spancontext" "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/swarm/swap" opentracing "github.com/opentracing/opentracing-go" ) @@ -136,6 +139,11 @@ type RetrieveRequestMsg struct { HopCount uint8 } +//TODO: what is the correct price +func (rrm *RetrieveRequestMsg) GetMsgPrice() (*big.Int, swap.EntryDirection) { + return big.NewInt(int64(4096)), swap.CreditEntry +} + func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req *RetrieveRequestMsg) error { log.Trace("received request", "peer", sp.ID(), "hash", req.Addr) handleRetrieveRequestMsgCount.Inc(1) diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index 3861cfcf6a..7576e91da9 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -34,6 +34,8 @@ import ( "github.com/ethereum/go-ethereum/swarm/pot" "github.com/ethereum/go-ethereum/swarm/state" "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/swarm/swap" + opentracing "github.com/opentracing/opentracing-go" ) const ( @@ -102,6 +104,12 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy RegisterSwarmSyncerServer(streamer, syncChunkStore) RegisterSwarmSyncerClient(streamer, syncChunkStore) + var err error + streamer.swap, err = swap.NewSwap(swap.NewDefaultSwapParams().Params) + if err != nil { + log.Error(err.Error()) + } + if options.DoSync { // latestIntC function ensures that // - receiving from the in chan is not blocked by processing inside the for loop @@ -381,7 +389,7 @@ func (r *Registry) Run(p *network.BzzPeer) error { } } - return sp.Run(sp.HandleMsg) + return sp.Run(sp.HandleAccountedMsg) } // updateSyncing subscribes to SYNC streams by iterating over the @@ -456,8 +464,16 @@ func (r *Registry) runProtocol(p *p2p.Peer, rw p2p.MsgReadWriter) error { return r.Run(bp) } +func (p *Peer) HandleAccountedMsg(ctx context.Context, msg interface{}) error { + err := p.handleMsg(ctx, msg) + if _, ok := msg.(swap.SwapAccountedMsgType); ok && err == nil { + p.streamer.swap.AccountForMsg(ctx, msg, p.ID()) + } + return err +} + // HandleMsg is the message handler that delegates incoming messages -func (p *Peer) HandleMsg(ctx context.Context, msg interface{}) error { +func (p *Peer) handleMsg(ctx context.Context, msg interface{}) error { switch msg := msg.(type) { case *SubscribeMsg: diff --git a/swarm/swap/api.go b/swarm/swap/api.go index 2de11d9efa..632a62433f 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -31,7 +31,7 @@ type APIMsg struct { // Additional public methods accessible through API for pss type API struct { - *Swap + *SwapProtocol } type SwapMetrics struct { @@ -40,8 +40,8 @@ type SwapMetrics struct { type Cheque struct { } -func NewAPI(swap *Swap) *API { - return &API{Swap: swap} +func NewAPI(swap *SwapProtocol) *API { + return &API{SwapProtocol: swap} } func (swapapi *API) Balance(ctx context.Context) (balance int, err error) { diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 63acbbe929..1744bc9ada 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -14,164 +14,186 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -// +build !nopssprotocol - package swap import ( + "context" + "fmt" "sync" - "time" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/protocols" - "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/swarm/log" ) const ( IsActiveProtocol = true ) -// Convenience wrapper for devp2p protocol messages for transport over pss -type ProtocolMsg struct { - Code uint64 - Size uint32 - Payload []byte - ReceivedAt time.Time +type SwapProtocol struct { + peersMu sync.RWMutex + peers map[discover.NodeID]*SwapPeer } -// Creates a ProtocolMsg -func NewProtocolMsg(code uint64, msg interface{}) ([]byte, error) { +// Peer is the Peer extension for the streaming protocol +type SwapPeer struct { + *protocols.Peer + swapProtocol *SwapProtocol +} - rlpdata, err := rlp.EncodeToBytes(msg) - if err != nil { - return nil, err +func NewSwapProtocol() *SwapProtocol { + proto := &SwapProtocol{ + peers: make(map[discover.NodeID]*SwapPeer), } + return proto +} - // TODO verify that nested structs cannot be used in rlp - smsg := &ProtocolMsg{ - Code: code, - Size: uint32(len(rlpdata)), - Payload: rlpdata, +// NewPeer is the constructor for Peer +func NewPeer(peer *protocols.Peer, swap *SwapProtocol) *SwapPeer { + p := &SwapPeer{ + Peer: peer, + swapProtocol: swap, } - - return rlp.EncodeToBytes(smsg) + return p } -// Protocol options to be passed to a new Protocol instance -// -// The parameters specify which encryption schemes to allow -type ProtocolParams struct { +type IssueChequeMsg struct { } -// Convenience object for emulation devp2p over pss -type Protocol struct { - proto *p2p.Protocol - spec *protocols.Spec - RWPoolMu sync.Mutex +type RedeemChequeMsg struct { } -/* -// Activates devp2p emulation over a specific pss topic -// -// One or both encryption schemes must be specified. If -// only one is specified, the protocol will not be valid -// for the other, and will make the message handler -// return errors -func RegisterProtocol(ps *Pss, topic *Topic, spec *protocols.Spec, targetprotocol *p2p.Protocol, options *ProtocolParams) (*Protocol, error) { - if !options.Asymmetric && !options.Symmetric { - return nil, fmt.Errorf("specify at least one of asymmetric or symmetric messaging mode") - } - pp := &Protocol{ - Pss: ps, - proto: targetprotocol, - topic: topic, - spec: spec, - pubKeyRWPool: make(map[string]p2p.MsgReadWriter), - symKeyRWPool: make(map[string]p2p.MsgReadWriter), - Asymmetric: options.Asymmetric, - Symmetric: options.Symmetric, - } - return pp, nil -} +///////////////////////////////////////////////////////////////////// +// SECTION: node.Service interface +///////////////////////////////////////////////////////////////////// -*/ -// Generic handler for incoming messages over devp2p emulation -// -// To be passed to pss.Register() -// -// Will run the protocol on a new incoming peer, provided that -// the encryption key of the message has a match in the internal -// pss keypool -// -// Fails if protocol is not valid for the message encryption scheme, -// if adding a new peer fails, or if the message is not a serialized -// p2p.Msg (which it always will be if it is sent from this object). -func (p *Protocol) Handle(msg []byte, peer *p2p.Peer, asymmetric bool, keyid string) error { +func (p *SwapProtocol) Start(srv *p2p.Server) error { + log.Debug("Started swap") return nil } -/* -// Runs an emulated pss Protocol on the specified peer, -// linked to a specific topic -// `key` and `asymmetric` specifies what encryption key -// to link the peer to. -// The key must exist in the pss store prior to adding the peer. -func (p *Protocol) AddPeer(peer *p2p.Peer, topic Topic, asymmetric bool, key string) (p2p.MsgReadWriter, error) { - rw := &PssReadWriter{ - Pss: p.Pss, - rw: make(chan p2p.Msg), - spec: p.spec, - topic: p.topic, - key: key, - } - if asymmetric { - rw.sendFunc = p.Pss.SendAsym - } else { - rw.sendFunc = p.Pss.SendSym - } - if asymmetric { - p.Pss.pubKeyPoolMu.Lock() - if _, ok := p.Pss.pubKeyPool[key]; !ok { - return nil, fmt.Errorf("asym key does not exist: %s", key) - } - p.Pss.pubKeyPoolMu.Unlock() - p.RWPoolMu.Lock() - p.pubKeyRWPool[key] = rw - p.RWPoolMu.Unlock() - } else { - p.Pss.symKeyPoolMu.Lock() - if _, ok := p.Pss.symKeyPool[key]; !ok { - return nil, fmt.Errorf("symkey does not exist: %s", key) - } - p.Pss.symKeyPoolMu.Unlock() - p.RWPoolMu.Lock() - p.symKeyRWPool[key] = rw - p.RWPoolMu.Unlock() - } - go func() { - err := p.proto.Run(peer, rw) - log.Warn(fmt.Sprintf("pss vprotocol quit on %v topic %v: %v", peer, topic, err)) - }() - return rw, nil +func (p *SwapProtocol) Stop() error { + log.Info("swap shutting down") + return nil } -func (p *Protocol) RemovePeer(asymmetric bool, key string) { - log.Debug("closing pss peer", "asym", asymmetric, "key", key) - p.RWPoolMu.Lock() - defer p.RWPoolMu.Unlock() - if asymmetric { - rw := p.pubKeyRWPool[key].(*PssReadWriter) - rw.closed = true - delete(p.pubKeyRWPool, key) - } else { - rw := p.symKeyRWPool[key].(*PssReadWriter) - rw.closed = true - delete(p.symKeyRWPool, key) +var swapSpec = &protocols.Spec{ + Name: swapProtocolName, + Version: swapVersion, + MaxMsgSize: defaultMaxMsgSize, + Messages: []interface{}{ + SwapMsg{}, + }, +} + +func (p *SwapProtocol) Protocols() []p2p.Protocol { + return []p2p.Protocol{ + { + Name: swapSpec.Name, + Version: swapSpec.Version, + Length: swapSpec.Length(), + Run: p.Run, + }, } } -// Uniform translation of protocol specifiers to topic -func ProtocolTopic(spec *protocols.Spec) Topic { - return BytesToTopic([]byte(fmt.Sprintf("%s:%d", spec.Name, spec.Version))) +func (swap *SwapProtocol) DebitByteCount(peer *SwapPeer, numberOfBytes int) error { + return nil +} + +func (swap *SwapProtocol) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { + p := protocols.NewPeer(peer, rw, swapSpec) + sp := NewPeer(p, swap) + swap.setPeer(sp) + defer swap.deletePeer(sp) + defer swap.Close() + return sp.Run(sp.handleSwapMsg) +} + +func (p *SwapProtocol) APIs() []rpc.API { + apis := []rpc.API{ + { + Namespace: "swap", + Version: "1.0", + Service: NewAPI(p), + Public: true, + }, + } + return apis +} + +//-------------------- + +func (swap *SwapProtocol) NodeInfo() interface{} { + return nil +} + +func (swap *SwapProtocol) PeerInfo(id discover.NodeID) interface{} { + return nil +} + +func (swap *SwapProtocol) Close() error { + return nil +} + +func (swap *SwapProtocol) getPeer(peerId discover.NodeID) *SwapPeer { + swap.peersMu.RLock() + defer swap.peersMu.RUnlock() + + return swap.peers[peerId] +} + +func (swap *SwapProtocol) setPeer(peer *SwapPeer) { + swap.peersMu.Lock() + defer swap.peersMu.Unlock() + + swap.peers[peer.ID()] = peer + metrics.GetOrRegisterGauge("registry.peers", nil).Update(int64(len(swap.peers))) +} + +func (swap *SwapProtocol) deletePeer(peer *SwapPeer) { + swap.peersMu.Lock() + defer swap.peersMu.Unlock() + + delete(swap.peers, peer.ID()) + metrics.GetOrRegisterGauge("registry.peers", nil).Update(int64(len(swap.peers))) + swap.peersMu.Unlock() +} + +func (swap *SwapProtocol) peersCount() (c int) { + swap.peersMu.Lock() + c = len(swap.peers) + swap.peersMu.Unlock() + return +} + +func (p *SwapPeer) handleSwapMsg(ctx context.Context, msg interface{}) error { + switch msg := msg.(type) { + + case *IssueChequeMsg: + return p.handleIssueChequeMsg(ctx, msg) + + case *RedeemChequeMsg: + return p.handleRedeemChequeMsg(ctx, msg) + + /* + case *QuitMsg: + return p.handleQuitMsg(msg) + */ + + default: + return fmt.Errorf("unknown message type: %T", msg) + } + return nil +} + +func (sp *SwapPeer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { + return err +} + +func (sp *SwapPeer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { + return err } -*/ diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 7b58331cbf..b67c53768e 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -32,9 +32,7 @@ import ( "github.com/ethereum/go-ethereum/contracts/chequebook/contract" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/protocols" - "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/swarm/log" whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" ) @@ -53,8 +51,8 @@ var ( autoDepositBuffer = big.NewInt(100000000000000) // buffer that is surplus for fork protection etc (wei) buyAt = big.NewInt(20000000000) // maximum chunk price host is willing to pay (wei) sellAt = big.NewInt(20000000000) // minimum chunk price host requires (wei) - payAt = 100 // threshold that triggers payment {request} (units) - dropAt = 10000 // threshold that triggers disconnect (units) + payAt = big.NewInt(4096 * 10000) // threshold that triggers payment {request} (bytes) + dropAt = big.NewInt(4096 * 10000) // threshold that triggers disconnect (bytes) ) const ( @@ -66,10 +64,38 @@ const ( // Swift Automatic Payments // a peer to peer micropayment system type Swap struct { - lock sync.Mutex // mutex for balance access - balance int // units of chunk/retrieval request - local *Params // local peer's swap parameters - remote *Profile // remote peer's swap profile + lock sync.RWMutex + peers map[discover.NodeID]*swapPeer + local *Params // local peer's swap parameters +} + +type EntryDirection bool + +const ( + DebitEntry EntryDirection = true + CreditEntry EntryDirection = false +) + +type SwapAccountedMsgType interface { + GetMsgPrice() (*big.Int, EntryDirection) +} + +func (swap *Swap) AccountForMsg(ctx context.Context, msg interface{}, peer discover.NodeID) error { + if accounted, ok := msg.(SwapAccountedMsgType); ok { + if _, exists := swap.peers[peer]; !exists { + swap.lock.Lock() + swap.peers[peer] = &swapPeer{ + peer: peer, + swapAccount: swap, + balance: big.NewInt(0), + } + swap.lock.Unlock() + } + price, direction := accounted.GetMsgPrice() + //TODO: Calculate total price and account + swap.peers[peer].AccountMsgForPeer(price, direction) + } + return nil } // Profile - public swap profile @@ -77,8 +103,8 @@ type Swap struct { type Profile struct { BuyAt *big.Int // accepted max price for chunk SellAt *big.Int // offered sale price for chunk - PayAt uint // threshold that triggers payment request - DropAt uint // threshold that triggers disconnect + PayAt *big.Int // threshold that triggers payment request + DropAt *big.Int // threshold that triggers disconnect } // Strategy encapsulates parameters relating to @@ -130,15 +156,41 @@ type PayProfile struct { lock sync.RWMutex } +type swapPeer struct { + lock sync.RWMutex + peer discover.NodeID + swapAccount *Swap + balance *big.Int +} + +func (sp *swapPeer) AccountMsgForPeer(price *big.Int, direction EntryDirection) { + sp.lock.Lock() + defer sp.lock.Unlock() + //the peer is being credited (in its favor), so its balance increases + if direction == CreditEntry { + sp.balance = sp.balance.Add(sp.balance, price) + //the peer is being debited (in local favor), so its balance decreases + } else if direction == DebitEntry { + sp.balance = sp.balance.Sub(sp.balance, price) + } + if sp.balance.Cmp(payAt) > -1 { + //TODO: Issue Cheque + } + if sp.balance.Cmp(dropAt) < 0 { + //TODO: Drop peer + } + log.Error(fmt.Sprintf("balance for peer %s: %s", sp.peer, sp.balance.String())) +} + // New - swap constructor func NewSwap(local *Params) (swap *Swap, err error) { swap = &Swap{ local: local, + peers: make(map[discover.NodeID]*swapPeer), } //swap.SetParams(local) - return } @@ -150,8 +202,8 @@ func NewDefaultSwapParams() *LocalProfile { Profile: &Profile{ BuyAt: buyAt, SellAt: sellAt, - PayAt: uint(payAt), - DropAt: uint(dropAt), + PayAt: payAt, + DropAt: dropAt, }, Strategy: &Strategy{ AutoCashInterval: autoCashInterval, @@ -179,70 +231,6 @@ func (lp *LocalProfile) Init(contract common.Address, prvkey *ecdsa.PrivateKey) } } -///////////////////////////////////////////////////////////////////// -// SECTION: node.Service interface -///////////////////////////////////////////////////////////////////// - -func (p *Swap) Start(srv *p2p.Server) error { - log.Debug("Started swap") - return nil -} - -func (p *Swap) Stop() error { - log.Info("swap shutting down") - return nil -} - -var swapSpec = &protocols.Spec{ - Name: swapProtocolName, - Version: swapVersion, - MaxMsgSize: defaultMaxMsgSize, - Messages: []interface{}{ - SwapMsg{}, - }, -} - -func (p *Swap) Protocols() []p2p.Protocol { - return []p2p.Protocol{ - { - Name: swapSpec.Name, - Version: swapSpec.Version, - Length: swapSpec.Length(), - Run: p.Run, - }, - } -} - -func (p *Swap) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { - pp := protocols.NewPeer(peer, rw, swapSpec) - /* - p.fwdPoolMu.Lock() - p.fwdPool[peer.Info().ID] = pp - p.fwdPoolMu.Unlock() - */ - return pp.Run(p.handleSwapMsg) -} - -func (p *Swap) APIs() []rpc.API { - apis := []rpc.API{ - { - Namespace: "swap", - Version: "1.0", - Service: NewAPI(p), - Public: true, - }, - } - return apis -} - -// Filters incoming messages for processing or forwarding. -// Check if address partially matches -// If yes, it CAN be for us, and we process it -// Only passes error to pss protocol handler if payload is not valid pssmsg -func (p *Swap) handleSwapMsg(ctx context.Context, msg interface{}) error { - return nil -} - // Chequebook get's chequebook from the localProfile func (lp *LocalProfile) Chequebook() *chequebook.Chequebook { defer lp.lock.Unlock() @@ -351,12 +339,13 @@ func (lp *LocalProfile) newChequebookFromContract(path string, backend chequeboo return nil } +/* // Add (n) // n > 0 called when promised/provided n units of service // n < 0 called when used/requested n units of service func (swap *Swap) Add(n int) error { - defer swap.lock.Unlock() - swap.lock.Lock() + //defer swap.lock.Unlock() + //swap.lock.Lock() swap.balance += n if !swap.Sells && swap.balance > 0 { log.Trace(fmt.Sprintf("<%v> remote peer cannot have debt (balance: %v)", swap.proto, swap.balance)) @@ -379,11 +368,12 @@ func (swap *Swap) Add(n int) error { // Balance accessor func (swap *Swap) Balance() int { - defer swap.lock.Unlock() - swap.lock.Lock() + //defer swap.lock.Unlock() + //swap.lock.Lock() return swap.balance } +/* // send (units) is called when payment is due // In case of insolvency no promise is issued and sent, safe against fraud // No return value: no error = payment is opportunistic = hang in till dropped @@ -431,3 +421,4 @@ func (swap *Swap) Receive(units int, promise Promise) error { return nil } +*/ diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 289ba028be..2be0c5013f 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -71,7 +71,7 @@ func newServiceNode(port int, httpport int, wsport int, modules ...string) (*nod return stack, nil } -func TestBasicSwap(t *testing.T) { +func TestSwapProtocol(t *testing.T) { // create the two nodes stack_one, err := newServiceNode(p2pPort, 0, 0) @@ -83,14 +83,11 @@ func TestBasicSwap(t *testing.T) { log.Crit("Create servicenode #2 fail", "err", err) } - instance, err := NewSwap(NewDefaultSwapParams().Params) - if err != nil { - t.Fatal("Couldn't create Swap instance") - } + instance := NewSwapProtocol() // wrapper function for servicenode to start the service swapsvc := func(ctx *node.ServiceContext) (node.Service, error) { return &API{ - Swap: instance, + SwapProtocol: instance, }, nil } diff --git a/swarm/swarm.go b/swarm/swarm.go index e2585c6de3..7c6adb5e74 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -209,10 +209,12 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e self.bzz = network.NewBzz(bzzconfig, to, stateStore, stream.Spec, self.streamer.Run) - self.swap, err = swap.NewSwap(config.Swap.Params) - if err != nil { - return nil, err - } + /* + self.swap, err = swap.NewSwap(config.Swap.Params) + if err != nil { + return nil, err + } + */ // Pss = postal service over swarm (devp2p over bzz) self.ps, err = pss.NewPss(to, config.Pss) if err != nil { @@ -368,12 +370,14 @@ func (self *Swarm) Start(srv *p2p.Server) error { } log.Info("Swarm network started", "bzzaddr", fmt.Sprintf("%x", self.bzz.Hive.BaseAddr())) - err = self.swap.Start(srv) - if err != nil { - log.Error("swap failed", "err", err) - return err - } - log.Debug("Swap accounting initialized") + /* + err = self.swap.Start(srv) + if err != nil { + log.Error("swap failed", "err", err) + return err + } + log.Debug("Swap accounting initialized") + */ if self.ps != nil { self.ps.Start(srv) From fac86b399f75c52efd30963f1b00b28340970697 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Tue, 14 Aug 2018 19:43:25 -0500 Subject: [PATCH 03/30] swarm/swap: attach swap to swarm, not registry --- swarm/network/stream/stream.go | 6 +- swarm/network_test.go | 115 ++++++++++++++++++++++++++++++++- swarm/swap/api.go | 7 +- swarm/swap/swap.go | 32 ++++++--- swarm/swap/swap_test.go | 18 ++++++ swarm/swarm.go | 6 -- 6 files changed, 160 insertions(+), 24 deletions(-) diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index 7576e91da9..106a4b6a9e 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -104,11 +104,7 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy RegisterSwarmSyncerServer(streamer, syncChunkStore) RegisterSwarmSyncerClient(streamer, syncChunkStore) - var err error - streamer.swap, err = swap.NewSwap(swap.NewDefaultSwapParams().Params) - if err != nil { - log.Error(err.Error()) - } + streamer.swap = swap if options.DoSync { // latestIntC function ensures that diff --git a/swarm/network_test.go b/swarm/network_test.go index aab1c05099..589f687cf7 100644 --- a/swarm/network_test.go +++ b/swarm/network_test.go @@ -40,9 +40,11 @@ import ( ) var ( - loglevel = flag.Int("loglevel", 2, "verbosity of logs") - longrunning = flag.Bool("longrunning", false, "do run long-running tests") - waitKademlia = flag.Bool("waitkademlia", false, "wait for healthy kademlia before checking files availability") + loglevel = flag.Int("loglevel", 2, "verbosity of logs") + longrunning = flag.Bool("longrunning", false, "do run long-running tests") + waitKademlia = flag.Bool("waitkademlia", false, "wait for healthy kademlia before checking files availability") + bucketKeySwap = simulation.BucketKey("swap") + bucketKeySwarm = simulation.BucketKey("swarm") ) func init() { @@ -373,6 +375,113 @@ func testSwarmNetwork(t *testing.T, o *testSwarmNetworkOptions, steps ...testSwa } } +func TestSwapNetworkSymmetricFileUpload(t *testing.T) { + nodeCount := 16 + + sim := simulation.New(map[string]simulation.ServiceFunc{ + "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { + config := api.NewConfig() + + dir, err := ioutil.TempDir("", "swap-network-test-node") + if err != nil { + return nil, nil, err + } + cleanup = func() { + err := os.RemoveAll(dir) + if err != nil { + log.Error("cleaning up swarm temp dir", "err", err) + } + } + + config.Path = dir + + privkey, err := crypto.GenerateKey() + if err != nil { + return nil, cleanup, err + } + + config.Init(privkey) + + swarm, err := NewSwarm(config, nil) + if err != nil { + return nil, cleanup, err + } + bucket.Store(bucketKeySwarm, swarm) + log.Info("new swarm", "bzzKey", config.BzzKey, "baseAddr", fmt.Sprintf("%x", swarm.bzz.BaseAddr())) + return swarm, cleanup, nil + }, + }) + defer sim.Close() + + ctx := context.Background() + files := make([]file, 0) + + var checkStatusM sync.Map + var nodeStatusM sync.Map + var totalFoundCount uint64 + + _, err := sim.AddNodesAndConnectChain(nodeCount) + if err != nil { + t.Fatal(err) + } + + result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { + nodeIDs := sim.UpNodeIDs() + shuffle(len(nodeIDs), func(i, j int) { + nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] + }) + for _, id := range nodeIDs { + key, data, err := uploadFile(sim.Service("swarm", id).(*Swarm)) + if err != nil { + return err + } + log.Trace("file uploaded", "node", id, "key", key.String()) + files = append(files, file{ + addr: key, + data: data, + nodeID: id, + }) + } + + if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { + return err + } + + // File retrieval check is repeated until all uploaded files are retrieved from all nodes + // or until the timeout is reached. + for { + if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 { + return nil + } + } + }) + + for _, node := range sim.NodeIDs() { + item, ok := sim.NodeItem(node, bucketKeySwarm) + if !ok { + log.Error("No swarm") + return + } + swarm := item.(*Swarm) + + for _, n := range sim.NodeIDs() { + if node == n { + continue + } + if swarm.swap.GetPeerBalance(n) != nil { + log.Error(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) + } else { + log.Error(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + } + } + } + + if result.Error != nil { + t.Fatal(result.Error) + } + log.Debug("test terminated") +} + // uploadFile, uploads a short file to the swarm instance // using the api.Put method. func uploadFile(swarm *Swarm) (storage.Address, string, error) { diff --git a/swarm/swap/api.go b/swarm/swap/api.go index 632a62433f..c731cb972b 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -18,9 +18,11 @@ package swap import ( "context" + "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/p2p/discover" ) // Wrapper for receiving pss messages when using the pss API @@ -32,6 +34,7 @@ type APIMsg struct { // Additional public methods accessible through API for pss type API struct { *SwapProtocol + *Swap } type SwapMetrics struct { @@ -44,8 +47,8 @@ func NewAPI(swap *SwapProtocol) *API { return &API{SwapProtocol: swap} } -func (swapapi *API) Balance(ctx context.Context) (balance int, err error) { - balance = 0 +func (swapapi *API) Balance(ctx context.Context, peer discover.NodeID) (balance *big.Int, err error) { + balance = big.NewInt(0) err = nil return } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index b67c53768e..62b8b2bdd0 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/swarm/log" + "github.com/ethereum/go-ethereum/swarm/state" whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" ) @@ -64,9 +65,10 @@ const ( // Swift Automatic Payments // a peer to peer micropayment system type Swap struct { - lock sync.RWMutex - peers map[discover.NodeID]*swapPeer - local *Params // local peer's swap parameters + stateStore state.Store + lock sync.RWMutex + peers map[discover.NodeID]*swapPeer + local *Params // local peer's swap parameters } type EntryDirection bool @@ -83,11 +85,14 @@ type SwapAccountedMsgType interface { func (swap *Swap) AccountForMsg(ctx context.Context, msg interface{}, peer discover.NodeID) error { if accounted, ok := msg.(SwapAccountedMsgType); ok { if _, exists := swap.peers[peer]; !exists { + balance := big.NewInt(0) + swap.stateStore.Get(peer.String()[:24]+"-swap", &balance) swap.lock.Lock() swap.peers[peer] = &swapPeer{ peer: peer, swapAccount: swap, - balance: big.NewInt(0), + balance: balance, + storeID: peer.String()[:24] + "-swap", } swap.lock.Unlock() } @@ -98,6 +103,13 @@ func (swap *Swap) AccountForMsg(ctx context.Context, msg interface{}, peer disco return nil } +func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { + if p, ok := swap.peers[peer]; ok { + return p.balance + } + return nil +} + // Profile - public swap profile // public parameters for SWAP, serializable config struct passed in handshake type Profile struct { @@ -161,6 +173,7 @@ type swapPeer struct { peer discover.NodeID swapAccount *Swap balance *big.Int + storeID string } func (sp *swapPeer) AccountMsgForPeer(price *big.Int, direction EntryDirection) { @@ -173,21 +186,24 @@ func (sp *swapPeer) AccountMsgForPeer(price *big.Int, direction EntryDirection) } else if direction == DebitEntry { sp.balance = sp.balance.Sub(sp.balance, price) } + //TODO: save to store here? init store? + sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) if sp.balance.Cmp(payAt) > -1 { //TODO: Issue Cheque } if sp.balance.Cmp(dropAt) < 0 { //TODO: Drop peer } - log.Error(fmt.Sprintf("balance for peer %s: %s", sp.peer, sp.balance.String())) + log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.peer, sp.balance.String())) } // New - swap constructor -func NewSwap(local *Params) (swap *Swap, err error) { +func NewSwap(local *Params, stateStore state.Store) (swap *Swap, err error) { swap = &Swap{ - local: local, - peers: make(map[discover.NodeID]*swapPeer), + local: local, + stateStore: stateStore, + peers: make(map[discover.NodeID]*swapPeer), } //swap.SetParams(local) diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 2be0c5013f..060161b897 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -17,16 +17,28 @@ package swap import ( + "context" + "crypto/rand" "flag" "fmt" + "io/ioutil" "os" "path/filepath" "sync" + "sync/atomic" "testing" + "time" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/simulations/adapters" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/swarm" + "github.com/ethereum/go-ethereum/swarm/api" + "github.com/ethereum/go-ethereum/swarm/network/simulation" + "github.com/ethereum/go-ethereum/swarm/storage" colorable "github.com/mattn/go-colorable" ) @@ -91,6 +103,12 @@ func TestSwapProtocol(t *testing.T) { }, nil } + streamersvc := func(ctx *node.ServiceContext) (node.Service, erro) { + return &stream.API{ + streamer: NewRegistry, + }, nil + } + // register adds the service to the services the servicenode starts when started err = stack_one.Register(swapsvc) if err != nil { diff --git a/swarm/swarm.go b/swarm/swarm.go index 7c6adb5e74..acf29777dd 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -209,12 +209,6 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e self.bzz = network.NewBzz(bzzconfig, to, stateStore, stream.Spec, self.streamer.Run) - /* - self.swap, err = swap.NewSwap(config.Swap.Params) - if err != nil { - return nil, err - } - */ // Pss = postal service over swarm (devp2p over bzz) self.ps, err = pss.NewPss(to, config.Pss) if err != nil { From 251e53e4180149686cb92d717da5adcf627f5465 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Wed, 15 Aug 2018 13:57:03 -0500 Subject: [PATCH 04/30] swarm/swap: swap implemented as protocols/protocol.Peer extension --- swarm/network/stream/delivery.go | 5 +- swarm/network/stream/peer.go | 6 +- swarm/network/stream/stream.go | 12 +-- swarm/swap/protocol.go | 54 +++++++------- swarm/swap/swap.go | 124 +++++++++++++++++++------------ 5 files changed, 113 insertions(+), 88 deletions(-) diff --git a/swarm/network/stream/delivery.go b/swarm/network/stream/delivery.go index 29e6a2b938..4b494f1e06 100644 --- a/swarm/network/stream/delivery.go +++ b/swarm/network/stream/delivery.go @@ -30,7 +30,6 @@ import ( "github.com/ethereum/go-ethereum/swarm/network" "github.com/ethereum/go-ethereum/swarm/spancontext" "github.com/ethereum/go-ethereum/swarm/storage" - "github.com/ethereum/go-ethereum/swarm/swap" opentracing "github.com/opentracing/opentracing-go" ) @@ -140,8 +139,8 @@ type RetrieveRequestMsg struct { } //TODO: what is the correct price -func (rrm *RetrieveRequestMsg) GetMsgPrice() (*big.Int, swap.EntryDirection) { - return big.NewInt(int64(4096)), swap.CreditEntry +func (rrm *RetrieveRequestMsg) GetMsgPrice() *big.Int { + return big.NewInt(int64(4096)) } func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req *RetrieveRequestMsg) error { diff --git a/swarm/network/stream/peer.go b/swarm/network/stream/peer.go index 89d135ad52..f7ad57b032 100644 --- a/swarm/network/stream/peer.go +++ b/swarm/network/stream/peer.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/swarm/spancontext" "github.com/ethereum/go-ethereum/swarm/state" "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/swarm/swap" opentracing "github.com/opentracing/opentracing-go" ) @@ -53,7 +54,8 @@ var ErrMaxPeerServers = errors.New("max peer servers") // Peer is the Peer extension for the streaming protocol type Peer struct { - *protocols.Peer + //*protocols.Peer + *swap.SwapPeer streamer *Registry pq *pq.PriorityQueue serverMu sync.RWMutex @@ -75,7 +77,7 @@ type WrappedPriorityMsg struct { // NewPeer is the constructor for Peer func NewPeer(peer *protocols.Peer, streamer *Registry) *Peer { p := &Peer{ - Peer: peer, + SwapPeer: swap.NewSwapPeer(peer, streamer.swap), pq: pq.New(int(PriorityQueue), PriorityQueueCap), streamer: streamer, servers: make(map[Stream]*server), diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index 106a4b6a9e..004c42d860 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -385,7 +385,7 @@ func (r *Registry) Run(p *network.BzzPeer) error { } } - return sp.Run(sp.HandleAccountedMsg) + return sp.RunAccountedProtocol(sp.HandleMsg) } // updateSyncing subscribes to SYNC streams by iterating over the @@ -460,16 +460,8 @@ func (r *Registry) runProtocol(p *p2p.Peer, rw p2p.MsgReadWriter) error { return r.Run(bp) } -func (p *Peer) HandleAccountedMsg(ctx context.Context, msg interface{}) error { - err := p.handleMsg(ctx, msg) - if _, ok := msg.(swap.SwapAccountedMsgType); ok && err == nil { - p.streamer.swap.AccountForMsg(ctx, msg, p.ID()) - } - return err -} - // HandleMsg is the message handler that delegates incoming messages -func (p *Peer) handleMsg(ctx context.Context, msg interface{}) error { +func (p *Peer) HandleMsg(ctx context.Context, msg interface{}) error { switch msg := msg.(type) { case *SubscribeMsg: diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 1744bc9ada..b0a27983ec 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -35,25 +35,25 @@ const ( type SwapProtocol struct { peersMu sync.RWMutex - peers map[discover.NodeID]*SwapPeer + peers map[discover.NodeID]*SwapProtocolPeer } // Peer is the Peer extension for the streaming protocol -type SwapPeer struct { +type SwapProtocolPeer struct { *protocols.Peer swapProtocol *SwapProtocol } func NewSwapProtocol() *SwapProtocol { proto := &SwapProtocol{ - peers: make(map[discover.NodeID]*SwapPeer), + peers: make(map[discover.NodeID]*SwapProtocolPeer), } return proto } // NewPeer is the constructor for Peer -func NewPeer(peer *protocols.Peer, swap *SwapProtocol) *SwapPeer { - p := &SwapPeer{ +func NewPeer(peer *protocols.Peer, swap *SwapProtocol) *SwapProtocolPeer { + p := &SwapProtocolPeer{ Peer: peer, swapProtocol: swap, } @@ -100,19 +100,6 @@ func (p *SwapProtocol) Protocols() []p2p.Protocol { } } -func (swap *SwapProtocol) DebitByteCount(peer *SwapPeer, numberOfBytes int) error { - return nil -} - -func (swap *SwapProtocol) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { - p := protocols.NewPeer(peer, rw, swapSpec) - sp := NewPeer(p, swap) - swap.setPeer(sp) - defer swap.deletePeer(sp) - defer swap.Close() - return sp.Run(sp.handleSwapMsg) -} - func (p *SwapProtocol) APIs() []rpc.API { apis := []rpc.API{ { @@ -125,7 +112,17 @@ func (p *SwapProtocol) APIs() []rpc.API { return apis } -//-------------------- +///////////////////////////////////////////////////////////////////// +// SECTION: p2p.protocol interface +///////////////////////////////////////////////////////////////////// +func (swap *SwapProtocol) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { + p := protocols.NewPeer(peer, rw, swapSpec) + sp := NewPeer(p, swap) + swap.setPeer(sp) + defer swap.deletePeer(sp) + defer swap.Close() + return sp.Run(sp.handleSwapMsg) +} func (swap *SwapProtocol) NodeInfo() interface{} { return nil @@ -135,18 +132,23 @@ func (swap *SwapProtocol) PeerInfo(id discover.NodeID) interface{} { return nil } -func (swap *SwapProtocol) Close() error { +//------------------------------------------------------------------------------------------ + +func (swap *SwapProtocol) DebitByteCount(peer *SwapProtocolPeer, numberOfBytes int) error { return nil } -func (swap *SwapProtocol) getPeer(peerId discover.NodeID) *SwapPeer { +func (swap *SwapProtocol) Close() { +} + +func (swap *SwapProtocol) getPeer(peerId discover.NodeID) *SwapProtocolPeer { swap.peersMu.RLock() defer swap.peersMu.RUnlock() return swap.peers[peerId] } -func (swap *SwapProtocol) setPeer(peer *SwapPeer) { +func (swap *SwapProtocol) setPeer(peer *SwapProtocolPeer) { swap.peersMu.Lock() defer swap.peersMu.Unlock() @@ -154,7 +156,7 @@ func (swap *SwapProtocol) setPeer(peer *SwapPeer) { metrics.GetOrRegisterGauge("registry.peers", nil).Update(int64(len(swap.peers))) } -func (swap *SwapProtocol) deletePeer(peer *SwapPeer) { +func (swap *SwapProtocol) deletePeer(peer *SwapProtocolPeer) { swap.peersMu.Lock() defer swap.peersMu.Unlock() @@ -170,7 +172,7 @@ func (swap *SwapProtocol) peersCount() (c int) { return } -func (p *SwapPeer) handleSwapMsg(ctx context.Context, msg interface{}) error { +func (p *SwapProtocolPeer) handleSwapMsg(ctx context.Context, msg interface{}) error { switch msg := msg.(type) { case *IssueChequeMsg: @@ -190,10 +192,10 @@ func (p *SwapPeer) handleSwapMsg(ctx context.Context, msg interface{}) error { return nil } -func (sp *SwapPeer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { +func (sp *SwapProtocolPeer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { return err } -func (sp *SwapPeer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { +func (sp *SwapProtocolPeer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { return err } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 62b8b2bdd0..36b2948677 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/state" whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" @@ -67,10 +68,19 @@ const ( type Swap struct { stateStore state.Store lock sync.RWMutex - peers map[discover.NodeID]*swapPeer + peers map[discover.NodeID]*SwapPeer local *Params // local peer's swap parameters } +type SwapPeer struct { + *protocols.Peer + lock sync.RWMutex + swapAccount *Swap + handlerFunc func(context.Context, interface{}) error + balance *big.Int + storeID string +} + type EntryDirection bool const ( @@ -79,26 +89,20 @@ const ( ) type SwapAccountedMsgType interface { - GetMsgPrice() (*big.Int, EntryDirection) + GetMsgPrice() *big.Int } -func (swap *Swap) AccountForMsg(ctx context.Context, msg interface{}, peer discover.NodeID) error { +func (sp *SwapPeer) RunAccountedProtocol(protocolHandler func(ctx context.Context, msg interface{}) error) error { + sp.handlerFunc = protocolHandler + + return sp.Run(sp.handleAccountedMsg) +} + +func (sp *SwapPeer) doAccountMsg(ctx context.Context, msg interface{}, direction EntryDirection) error { if accounted, ok := msg.(SwapAccountedMsgType); ok { - if _, exists := swap.peers[peer]; !exists { - balance := big.NewInt(0) - swap.stateStore.Get(peer.String()[:24]+"-swap", &balance) - swap.lock.Lock() - swap.peers[peer] = &swapPeer{ - peer: peer, - swapAccount: swap, - balance: balance, - storeID: peer.String()[:24] + "-swap", - } - swap.lock.Unlock() - } - price, direction := accounted.GetMsgPrice() + price := accounted.GetMsgPrice() //TODO: Calculate total price and account - swap.peers[peer].AccountMsgForPeer(price, direction) + sp.AccountMsgForPeer(price, direction) } return nil } @@ -110,6 +114,61 @@ func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { return nil } +func (sp *SwapPeer) handleAccountedMsg(ctx context.Context, msg interface{}) error { + err := sp.handlerFunc(ctx, msg) + if _, ok := msg.(SwapAccountedMsgType); ok && err == nil { + sp.doAccountMsg(ctx, msg, CreditEntry) + } + return err +} + +func (sp *SwapPeer) Send(ctx context.Context, msg interface{}) error { + err := sp.Peer.Send(ctx, msg) + if _, ok := msg.(SwapAccountedMsgType); ok && err == nil { + sp.doAccountMsg(ctx, msg, DebitEntry) + } + return err +} + +//The balance is accounted from the point of view of the local node +//Thus, we credit the balance and increase it when the amount is in favor of the local node +//We debit the balance and decrease it when the amount is in favor of the remote peer +func (sp *SwapPeer) AccountMsgForPeer(price *big.Int, direction EntryDirection) { + sp.lock.Lock() + defer sp.lock.Unlock() + //local node is being credited (in its favor), so its balance increases + if direction == CreditEntry { + sp.balance = sp.balance.Add(sp.balance, price) + //local node is being debited (in favor of remote peer), so its balance decreases + } else if direction == DebitEntry { + sp.balance = sp.balance.Sub(sp.balance, price) + } + //TODO: save to store here? init store? + sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) + if sp.balance.Cmp(payAt) > -1 { + //TODO: Issue Cheque + } + if sp.balance.Cmp(dropAt) < 0 { + //TODO: Drop peer + } + log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.ID(), sp.balance.String())) +} + +func NewSwapPeer(peer *protocols.Peer, swap *Swap) *SwapPeer { + balance := big.NewInt(0) + swap.stateStore.Get(peer.String()[:24]+"-swap", &balance) + sp := &SwapPeer{ + Peer: peer, + swapAccount: swap, + balance: balance, + storeID: peer.String()[:24] + "-swap", + } + swap.lock.Lock() + defer swap.lock.Unlock() + swap.peers[peer.ID()] = sp + return sp +} + // Profile - public swap profile // public parameters for SWAP, serializable config struct passed in handshake type Profile struct { @@ -168,42 +227,13 @@ type PayProfile struct { lock sync.RWMutex } -type swapPeer struct { - lock sync.RWMutex - peer discover.NodeID - swapAccount *Swap - balance *big.Int - storeID string -} - -func (sp *swapPeer) AccountMsgForPeer(price *big.Int, direction EntryDirection) { - sp.lock.Lock() - defer sp.lock.Unlock() - //the peer is being credited (in its favor), so its balance increases - if direction == CreditEntry { - sp.balance = sp.balance.Add(sp.balance, price) - //the peer is being debited (in local favor), so its balance decreases - } else if direction == DebitEntry { - sp.balance = sp.balance.Sub(sp.balance, price) - } - //TODO: save to store here? init store? - sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) - if sp.balance.Cmp(payAt) > -1 { - //TODO: Issue Cheque - } - if sp.balance.Cmp(dropAt) < 0 { - //TODO: Drop peer - } - log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.peer, sp.balance.String())) -} - // New - swap constructor func NewSwap(local *Params, stateStore state.Store) (swap *Swap, err error) { swap = &Swap{ local: local, stateStore: stateStore, - peers: make(map[discover.NodeID]*swapPeer), + peers: make(map[discover.NodeID]*SwapPeer), } //swap.SetParams(local) From e46cac80b44cfe01894b9e190c50b1568e3c2ab5 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 16 Aug 2018 17:30:38 -0500 Subject: [PATCH 05/30] swarm/swap: insufficient funds checks and first working test --- swarm/network_test.go | 38 +++++++++- swarm/swap/swap.go | 157 +++++++++++++++++++++++++++++++++--------- 2 files changed, 158 insertions(+), 37 deletions(-) diff --git a/swarm/network_test.go b/swarm/network_test.go index 589f687cf7..629095c936 100644 --- a/swarm/network_test.go +++ b/swarm/network_test.go @@ -21,6 +21,7 @@ import ( "flag" "fmt" "io/ioutil" + "math/big" "math/rand" "os" "sync" @@ -43,6 +44,7 @@ var ( loglevel = flag.Int("loglevel", 2, "verbosity of logs") longrunning = flag.Bool("longrunning", false, "do run long-running tests") waitKademlia = flag.Bool("waitkademlia", false, "wait for healthy kademlia before checking files availability") + printStats = flag.Bool("printstats", false, "print accounting stats to STDOUT") bucketKeySwap = simulation.BucketKey("swap") bucketKeySwarm = simulation.BucketKey("swarm") ) @@ -456,6 +458,8 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { } }) + balancesMap := make(map[discover.NodeID]map[discover.NodeID]*big.Int) + for _, node := range sim.NodeIDs() { item, ok := sim.NodeItem(node, bucketKeySwarm) if !ok { @@ -464,14 +468,42 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { } swarm := item.(*Swarm) + subBalances := make(map[discover.NodeID]*big.Int) + for _, n := range sim.NodeIDs() { if node == n { continue } - if swarm.swap.GetPeerBalance(n) != nil { - log.Error(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) + balance := swarm.swap.GetPeerBalance(n) + if balance != nil { + subBalances[n] = balance + log.Debug(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) } else { - log.Error(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + } + } + balancesMap[node] = subBalances + } + + if *printStats { + for k, v := range balancesMap { + fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) + for kk, vv := range v { + fmt.Println(fmt.Sprintf(".........with node %s: balance %s", kk.TerminalString(), vv.String())) + } + } + } + + for k, mapForK := range balancesMap { + for n, balanceKwithN := range mapForK { + for subK, mapForSubK := range balancesMap { + if n == subK { + log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) + log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) + if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { + log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + } + } } } } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 36b2948677..7570ea486d 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -19,6 +19,7 @@ package swap import ( "context" "crypto/ecdsa" + "errors" "fmt" "math/big" "os" @@ -54,7 +55,10 @@ var ( buyAt = big.NewInt(20000000000) // maximum chunk price host is willing to pay (wei) sellAt = big.NewInt(20000000000) // minimum chunk price host requires (wei) payAt = big.NewInt(4096 * 10000) // threshold that triggers payment {request} (bytes) - dropAt = big.NewInt(4096 * 10000) // threshold that triggers disconnect (bytes) + dropAt = big.NewInt(-4096 * 10000) // threshold that triggers disconnect (bytes) + + ErrInsufficientFunds = errors.New("Insufficient funds") + ErrNotAccountedMsg = errors.New("Message does not need accounting") ) const ( @@ -92,21 +96,17 @@ type SwapAccountedMsgType interface { GetMsgPrice() *big.Int } +//Handler for received messages func (sp *SwapPeer) RunAccountedProtocol(protocolHandler func(ctx context.Context, msg interface{}) error) error { + //the `peer.Run` function is a loop, so in order to pre-/post-process a message with accounting, + //we need to save the actual handler sp.handlerFunc = protocolHandler + //then run the handler loop function return sp.Run(sp.handleAccountedMsg) } -func (sp *SwapPeer) doAccountMsg(ctx context.Context, msg interface{}, direction EntryDirection) error { - if accounted, ok := msg.(SwapAccountedMsgType); ok { - price := accounted.GetMsgPrice() - //TODO: Calculate total price and account - sp.AccountMsgForPeer(price, direction) - } - return nil -} - +//get a peer's balance func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { if p, ok := swap.peers[peer]; ok { return p.balance @@ -114,48 +114,137 @@ func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { return nil } +//Handle a received message; this is the handler loop function. +//Check if it needs accounting, and if yes, apply accounting logic: +//Check for sufficient funds, perform operation, then account func (sp *SwapPeer) handleAccountedMsg(ctx context.Context, msg interface{}) error { - err := sp.handlerFunc(ctx, msg) - if _, ok := msg.(SwapAccountedMsgType); ok && err == nil { - sp.doAccountMsg(ctx, msg, CreditEntry) + var err error + var price *big.Int + + //the message is one which needs accounting... + if _, ok := msg.(SwapAccountedMsgType); ok { + //..so first check if there are enough funds for the operation available + //(for crediting, this means if we are not essentially "overdrafting", or crossing the threshold) + price, err = sp.checkAvailableFunds(ctx, msg, CreditEntry) + //if not (or some other error occured), return error + if err != nil { + //also, if the error is indeed insufficient funds, then disconnect the peer + if err == ErrInsufficientFunds { + log.Error("Insufficient funds, dropping peer") + sp.Drop(err) + } + return err + } + //at this point we know there are sufficient funds, so process the message + err = sp.handlerFunc(ctx, msg) + if err == nil { + //and if no errors occurred, finally book the entry + sp.AccountMsgForPeer(ctx, msg, price, CreditEntry) + } + } else { + //this message doesn't need accounting, so just process it + err = sp.handlerFunc(ctx, msg) } return err } +//Send a message +//Check if it needs accounting, and if yes, apply accounting logic: +//Check for sufficient funds, perform operation, then account func (sp *SwapPeer) Send(ctx context.Context, msg interface{}) error { - err := sp.Peer.Send(ctx, msg) - if _, ok := msg.(SwapAccountedMsgType); ok && err == nil { - sp.doAccountMsg(ctx, msg, DebitEntry) + var err error + var price *big.Int + + //the message is one which needs accounting... + if _, ok := msg.(SwapAccountedMsgType); ok { + //..so first check if there are enough funds for the operation available + price, err = sp.checkAvailableFunds(ctx, msg, DebitEntry) + //if not (or some other error occured), return error + if err != nil { + //also, if the error is indeed insufficient funds, then disconnect the peer + if err == ErrInsufficientFunds { + log.Error("Insufficient funds, dropping peer") + sp.Drop(err) + } + return err + } + //at this point we know there are sufficient funds, so process the message + err = sp.Peer.Send(ctx, msg) + if err == nil { + //and if no errors occurred, finally book the entry + sp.AccountMsgForPeer(ctx, msg, price, DebitEntry) + } + } else { + //this message doesn't need accounting, so just process it + err = sp.Peer.Send(ctx, msg) } return err } +//check that the operation has enough funds available +func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, direction EntryDirection) (*big.Int, error) { + sp.lock.Lock() + defer sp.lock.Unlock() + + if accounted, ok := msg.(SwapAccountedMsgType); ok { + price := accounted.GetMsgPrice() + //local node is being credited (in its favor), so check upper limit + if direction == CreditEntry { + checkBalance := sp.balance.Add(sp.balance, price) + //(checkBalance *Int) Cmp(payAt) + // -1 if checkBalance < payAt + // 0 if checkBalance == payAt + // +1 if checkBalance > payAt + if checkBalance.Cmp(payAt) == 1 { + return nil, ErrInsufficientFunds + } + } else if direction == DebitEntry { + //(checkBalance *Int) Cmp(dropAt) + // -1 if checkBalance < dropAt + // 0 if checkBalance == dropAt + // +1 if checkBalance > dropAt + checkBalance := sp.balance.Sub(sp.balance, price) + if checkBalance.Cmp(dropAt) == -1 { + return nil, ErrInsufficientFunds + } + } + return price, nil + } + return nil, ErrNotAccountedMsg +} + //The balance is accounted from the point of view of the local node //Thus, we credit the balance and increase it when the amount is in favor of the local node //We debit the balance and decrease it when the amount is in favor of the remote peer -func (sp *SwapPeer) AccountMsgForPeer(price *big.Int, direction EntryDirection) { - sp.lock.Lock() - defer sp.lock.Unlock() - //local node is being credited (in its favor), so its balance increases - if direction == CreditEntry { - sp.balance = sp.balance.Add(sp.balance, price) - //local node is being debited (in favor of remote peer), so its balance decreases - } else if direction == DebitEntry { - sp.balance = sp.balance.Sub(sp.balance, price) +func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, price *big.Int, direction EntryDirection) { + if _, ok := msg.(SwapAccountedMsgType); ok { + sp.lock.Lock() + defer sp.lock.Unlock() + //local node is being credited (in its favor), so its balance increases + if direction == CreditEntry { + //NOTE: do we need to check for sufficient funds again? + //operations are not atomic/transactional, so balance may have changed in the meanwhile! + sp.balance = sp.balance.Add(sp.balance, price) + //local node is being debited (in favor of remote peer), so its balance decreases + } else if direction == DebitEntry { + sp.balance = sp.balance.Sub(sp.balance, price) + } + //TODO: save to store here? init store? + sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) + if sp.balance.Cmp(payAt) > -1 { + //TODO: Issue Cheque + } + if sp.balance.Cmp(dropAt) < 0 { + //TODO: Drop peer + } + log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.ID(), sp.balance.String())) } - //TODO: save to store here? init store? - sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) - if sp.balance.Cmp(payAt) > -1 { - //TODO: Issue Cheque - } - if sp.balance.Cmp(dropAt) < 0 { - //TODO: Drop peer - } - log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.ID(), sp.balance.String())) } +//Create a new swap accounted peer func NewSwapPeer(peer *protocols.Peer, swap *Swap) *SwapPeer { balance := big.NewInt(0) + //check if there is one already in the stateStore and load it swap.stateStore.Get(peer.String()[:24]+"-swap", &balance) sp := &SwapPeer{ Peer: peer, From 51b99805823838399c55c317c67f7b10b16ef90c Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 16 Aug 2018 18:26:52 -0500 Subject: [PATCH 06/30] swarm/swap: corrected limit handling --- swarm/swap/protocol.go | 7 ++-- swarm/swap/swap.go | 32 +++++++++++++++--- swarm/swap/swap_test.go | 74 ++++++++++++++++++++++++----------------- 3 files changed, 73 insertions(+), 40 deletions(-) diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index b0a27983ec..4846750c9b 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -133,11 +133,6 @@ func (swap *SwapProtocol) PeerInfo(id discover.NodeID) interface{} { } //------------------------------------------------------------------------------------------ - -func (swap *SwapProtocol) DebitByteCount(peer *SwapProtocolPeer, numberOfBytes int) error { - return nil -} - func (swap *SwapProtocol) Close() { } @@ -193,9 +188,11 @@ func (p *SwapProtocolPeer) handleSwapMsg(ctx context.Context, msg interface{}) e } func (sp *SwapProtocolPeer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { + log.Debug("SwapProtocolPeer: handleIssueChequeMsg") return err } func (sp *SwapProtocolPeer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { + log.Debug("SwapProtocolPeer: handleRedeemChequeMsg") return err } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 7570ea486d..eb2e07c805 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -54,8 +54,8 @@ var ( autoDepositBuffer = big.NewInt(100000000000000) // buffer that is surplus for fork protection etc (wei) buyAt = big.NewInt(20000000000) // maximum chunk price host is willing to pay (wei) sellAt = big.NewInt(20000000000) // minimum chunk price host requires (wei) - payAt = big.NewInt(4096 * 10000) // threshold that triggers payment {request} (bytes) - dropAt = big.NewInt(-4096 * 10000) // threshold that triggers disconnect (bytes) + payAt = big.NewInt(-4096 * 10000) // threshold that triggers payment {request} (bytes) + dropAt = big.NewInt(-4096 * 12000) // threshold that triggers disconnect (bytes) ErrInsufficientFunds = errors.New("Insufficient funds") ErrNotAccountedMsg = errors.New("Message does not need accounting") @@ -190,6 +190,10 @@ func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, di price := accounted.GetMsgPrice() //local node is being credited (in its favor), so check upper limit if direction == CreditEntry { + //TODO: is there are a check needed here? + //It should actually have been done on the client side, the debitor! + //creditor could theoretically go over payAt, but if well done, + //should have been checked on the client side so this shouldn't happen? checkBalance := sp.balance.Add(sp.balance, price) //(checkBalance *Int) Cmp(payAt) // -1 if checkBalance < payAt @@ -199,6 +203,11 @@ func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, di return nil, ErrInsufficientFunds } } else if direction == DebitEntry { + //NOTE: ErrInsufficientFunds should only be returned + //if the dropAt is exceeded, but should be ignored for payAt, + //as there is a "clemency" margin between triggering the check + //and actually disconnecting the peer + //(checkBalance *Int) Cmp(dropAt) // -1 if checkBalance < dropAt // 0 if checkBalance == dropAt @@ -231,16 +240,29 @@ func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, pric } //TODO: save to store here? init store? sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) - if sp.balance.Cmp(payAt) > -1 { - //TODO: Issue Cheque + //(sp.balance *Int) Cmp(payAt) + // -1 if sp.balance < payAt + // 0 if sp.balance == payAt + // +1 if sp.balance > payAt + if sp.balance.Cmp(payAt) > 1 { + err := sp.issueCheque(ctx) + if err != nil { + //TODO: special error handling, as at this point the accounting has been done + //but the cheque could not be sent? + } } if sp.balance.Cmp(dropAt) < 0 { - //TODO: Drop peer + sp.Drop(ErrInsufficientFunds) } log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.ID(), sp.balance.String())) } } +func (sp *SwapPeer) issueCheque(ctx context.Context) error { + msg := IssueChequeMsg{} + return sp.Send(ctx, msg) +} + //Create a new swap accounted peer func NewSwapPeer(peer *protocols.Peer, swap *Swap) *SwapPeer { balance := big.NewInt(0) diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 060161b897..895134cbd3 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -50,6 +50,17 @@ var ( loglevel = flag.Int("loglevel", 2, "verbosity of logs") ) +type testExceedsPayAtMsg struct{} +type testExceedsDropAtMsg struct{} + +func (tmsg *testExceedsPayAtMsg) GetMsgPrice() *big.Int { + return payAt.Sub(big.NewInt(1)) +} + +func (tmsg *testExceedsDropAtMsg) GetMsgPrice() *big.Int { + return dropAt.Sub(big.NewInt(1)) +} + func init() { flag.Parse() @@ -57,30 +68,13 @@ func init() { log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) } -func newServiceNode(port int, httpport int, wsport int, modules ...string) (*node.Node, error) { - cfg := &node.DefaultConfig - cfg.P2P.ListenAddr = fmt.Sprintf(":%d", port) - cfg.P2P.EnableMsgEvents = true - cfg.P2P.NoDiscovery = true - cfg.IPCPath = ipcpath - cfg.DataDir = fmt.Sprintf("%s%d", datadirPrefix, port) - if httpport > 0 { - cfg.HTTPHost = node.DefaultHTTPHost - cfg.HTTPPort = httpport +func TestLimits(t *testing.T) { + if dropAt >= payAt { + t.Fatal(fmt.Sprintf("dropAt limit is not lower than payAt limit, dropAt: %s, payAt: %s", dropAt.String(), payAt.String())) } - if wsport > 0 { - cfg.WSHost = node.DefaultWSHost - cfg.WSPort = wsport - cfg.WSOrigins = []string{"*"} - for i := 0; i < len(modules); i++ { - cfg.WSModules = append(cfg.WSModules, modules[i]) - } - } - stack, err := node.New(cfg) - if err != nil { - return nil, fmt.Errorf("ServiceNode create fail: %v", err) - } - return stack, nil +} + +func TestExceedsPayAt(t *testing.T) { } func TestSwapProtocol(t *testing.T) { @@ -103,12 +97,6 @@ func TestSwapProtocol(t *testing.T) { }, nil } - streamersvc := func(ctx *node.ServiceContext) (node.Service, erro) { - return &stream.API{ - streamer: NewRegistry, - }, nil - } - // register adds the service to the services the servicenode starts when started err = stack_one.Register(swapsvc) if err != nil { @@ -182,7 +170,7 @@ func TestSwapProtocol(t *testing.T) { // when we get the add event, we know we are connected ev := <-eventOneC if ev.Type != "add" { - log.Error("server #1 expected peer add", "eventtype", ev.Type) + log.Error("server #1 expected peer add", "eventtype", ev.Type) stackW.Done() return } @@ -288,3 +276,29 @@ func TestSwapProtocol(t *testing.T) { stack_two.Stop() */ } + +func newServiceNode(port int, httpport int, wsport int, modules ...string) (*node.Node, error) { + cfg := &node.DefaultConfig + cfg.P2P.ListenAddr = fmt.Sprintf(":%d", port) + cfg.P2P.EnableMsgEvents = true + cfg.P2P.NoDiscovery = true + cfg.IPCPath = ipcpath + cfg.DataDir = fmt.Sprintf("%s%d", datadirPrefix, port) + if httpport > 0 { + cfg.HTTPHost = node.DefaultHTTPHost + cfg.HTTPPort = httpport + } + if wsport > 0 { + cfg.WSHost = node.DefaultWSHost + cfg.WSPort = wsport + cfg.WSOrigins = []string{"*"} + for i := 0; i < len(modules); i++ { + cfg.WSModules = append(cfg.WSModules, modules[i]) + } + } + stack, err := node.New(cfg) + if err != nil { + return nil, fmt.Errorf("ServiceNode create fail: %v", err) + } + return stack, nil +} From 9d9063f4a8ae4d2bb10ee97b33af9bfe87c93966 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 16 Aug 2018 22:20:45 -0500 Subject: [PATCH 07/30] swarm/swap: improved tests, bug fixes --- swarm/swap/swap.go | 2 +- swarm/swap/swap_test.go | 116 +++++++++++++++++++++++++++++++++++----- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index eb2e07c805..5235d4f596 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -212,7 +212,7 @@ func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, di // -1 if checkBalance < dropAt // 0 if checkBalance == dropAt // +1 if checkBalance > dropAt - checkBalance := sp.balance.Sub(sp.balance, price) + checkBalance := sp.balance.Sub(sp.balance, price.Abs(price)) if checkBalance.Cmp(dropAt) == -1 { return nil, ErrInsufficientFunds } diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 895134cbd3..62cc9e17c9 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -18,27 +18,22 @@ package swap import ( "context" - "crypto/rand" "flag" "fmt" "io/ioutil" + "math/big" "os" "path/filepath" "sync" - "sync/atomic" "testing" - "time" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/p2p/simulations/adapters" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm" - "github.com/ethereum/go-ethereum/swarm/api" - "github.com/ethereum/go-ethereum/swarm/network/simulation" - "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/swarm/state" colorable "github.com/mattn/go-colorable" ) @@ -50,15 +45,27 @@ var ( loglevel = flag.Int("loglevel", 2, "verbosity of logs") ) +var testSpec = &protocols.Spec{ + Name: "swapTestSpec", + Version: 1, + MaxMsgSize: 10 * 1024 * 1024, + Messages: []interface{}{ + testExceedsPayAtMsg{}, + testExceedsDropAtMsg{}, + }, +} + type testExceedsPayAtMsg struct{} type testExceedsDropAtMsg struct{} func (tmsg *testExceedsPayAtMsg) GetMsgPrice() *big.Int { - return payAt.Sub(big.NewInt(1)) + diff := &big.Int{} + return diff.Sub(payAt, big.NewInt(1)) } func (tmsg *testExceedsDropAtMsg) GetMsgPrice() *big.Int { - return dropAt.Sub(big.NewInt(1)) + diff := &big.Int{} + return diff.Sub(dropAt, big.NewInt(1)) } func init() { @@ -69,12 +76,97 @@ func init() { } func TestLimits(t *testing.T) { - if dropAt >= payAt { + if dropAt.Cmp(payAt) > -1 { t.Fatal(fmt.Sprintf("dropAt limit is not lower than payAt limit, dropAt: %s, payAt: %s", dropAt.String(), payAt.String())) } } func TestExceedsPayAt(t *testing.T) { + dir, err := ioutil.TempDir("", "swap_test_store") + if err != nil { + panic(err) + } + defer os.RemoveAll(dir) + stateStore, err2 := state.NewDBStore(dir) + if err2 != nil { + panic(err2) + } + swap, err3 := NewSwap(NewDefaultSwapParams().Params, stateStore) + if err3 != nil { + t.Fatal(err3) + } + id := adapters.RandomNodeConfig().ID + testPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), nil, nil) + sp := NewSwapPeer(testPeer, swap) + sp.handlerFunc = msgHandler + ctx := context.Background() + sp.handleAccountedMsg(ctx, &testExceedsPayAtMsg{}) +} + +func TestExceedsDropAt(t *testing.T) { + dir, err := ioutil.TempDir("", "swap_test_store") + if err != nil { + panic(err) + } + defer os.RemoveAll(dir) + stateStore, err2 := state.NewDBStore(dir) + if err2 != nil { + panic(err2) + } + swap, err3 := NewSwap(NewDefaultSwapParams().Params, stateStore) + if err3 != nil { + t.Fatal(err3) + } + id := adapters.RandomNodeConfig().ID + testPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), nil, nil) + sp := NewSwapPeer(testPeer, swap) + sp.handlerFunc = msgHandler + ctx := context.Background() + err = sp.Send(ctx, &testExceedsDropAtMsg{}) + if err != ErrInsufficientFunds { + t.Fatal("Expected test to fail with insufficient funds, but it didn't") + } +} + +func TestProtocol(t *testing.T) { + /* + dir, err := ioutil.TempDir("", "swap_test_store") + if err != nil { + panic(err) + } + defer os.RemoveAll(dir) + + swap := NewSwap(NewDefaultSwapParams().Params, state.NewDBStore(dir)) + conf := adapters.RandomNodeConfig() + + protocol := func(p *p2p.Peer, rw p2p.MsgReadWriter) error { + peer := protocols.NewPeer(p, rw, testSpec) + runProtocol(peer, swap) + } + protocolTester := p2ptest.NewProtocolTester(t, conf.ID, 2, protocol) + protocolTester.TestExchanges(p2p.TestExchange{ + Label: "", + Expects: []p2ptest.Expect{ + { + Code: 5, + Msg: &RetrieveRequestMsg{ + Addr: hash0[:], + SkipCheck: true, + }, + Peer: peerID, + }, + }, + }) + */ +} + +func runProtocol(peer *protocols.Peer, swap *Swap) { + sp := NewSwapPeer(peer, swap) + sp.Peer.Run(msgHandler) +} + +func msgHandler(ctx context.Context, msg interface{}) error { + return nil } func TestSwapProtocol(t *testing.T) { From 6f27cf5ac4df794c5004e3571d14fd46ed68b6f8 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Fri, 17 Aug 2018 15:28:21 -0500 Subject: [PATCH 08/30] swarm/swap: very early cheques and test improvements --- swarm/swap/api.go | 32 ++-- swarm/swap/chequemanager.go | 65 +++++++ swarm/swap/protocol.go | 5 +- swarm/swap/swap.go | 26 +-- swarm/swap/swap_test.go | 346 +++++++++++++----------------------- 5 files changed, 223 insertions(+), 251 deletions(-) create mode 100644 swarm/swap/chequemanager.go diff --git a/swarm/swap/api.go b/swarm/swap/api.go index c731cb972b..960530e870 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -18,13 +18,17 @@ package swap import ( "context" + "errors" "math/big" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/p2p/discover" ) +var ( + ErrNoSuchPeerAccounting = errors.New("No accounting with that peer") +) + // Wrapper for receiving pss messages when using the pss API // providing access to sender of message type APIMsg struct { @@ -34,32 +38,32 @@ type APIMsg struct { // Additional public methods accessible through API for pss type API struct { *SwapProtocol - *Swap } +//TODO: define metrics type SwapMetrics struct { } -type Cheque struct { -} - func NewAPI(swap *SwapProtocol) *API { return &API{SwapProtocol: swap} } -func (swapapi *API) Balance(ctx context.Context, peer discover.NodeID) (balance *big.Int, err error) { +func (swapapi *API) BalanceWithPeer(ctx context.Context, peer discover.NodeID) (balance *big.Int, err error) { + balance = swapapi.swap.peers[peer].balance + if balance == nil { + err = ErrNoSuchPeerAccounting + } + return +} + +func (swapapi *API) Balance(ctx context.Context) (balance *big.Int, err error) { balance = big.NewInt(0) - err = nil + for _, peer := range swapapi.swap.peers { + balance.Add(balance, peer.balance) + } return } func (swapapi *API) GetSwapMetrics() (*SwapMetrics, error) { return nil, nil } - -func (swapapi *API) IssueCheque(recipient *common.Address) (*Cheque, error) { - return nil, nil -} - -func (swapapi *API) RedeemCheque(cheque *Cheque) { -} diff --git a/swarm/swap/chequemanager.go b/swarm/swap/chequemanager.go new file mode 100644 index 0000000000..a9e4494b0a --- /dev/null +++ b/swarm/swap/chequemanager.go @@ -0,0 +1,65 @@ +// Copyright 2018 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 + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/swarm/state" +) + +type ChequeManager struct { + stateStore state.Store + serialPerNode map[discover.NodeID]uint64 + openDebitCheques map[discover.NodeID][]*Cheque + openCreditCheques map[discover.NodeID][]*Cheque +} + +type Cheque struct { + serial uint64 + timeout uint64 + amount *big.Int + sumCumulated *big.Int + beneficiary discover.NodeID //this should probably be common.Address? +} + +func NewChequeManager(stateStore state.Store) *ChequeManager { + return &ChequeManager{ + stateStore: stateStore, + //TODO: restore from state store + serialPerNode: make(map[discover.NodeID]uint64), + openDebitCheques: make(map[discover.NodeID][]*Cheque), + openCreditCheques: make(map[discover.NodeID][]*Cheque), + } +} + +func (mgr *ChequeManager) CreateCheque(beneficiary discover.NodeID, amount *big.Int) *Cheque { + mgr.serialPerNode[beneficiary]++ + cheque := &Cheque{ + serial: mgr.serialPerNode[beneficiary], + beneficiary: beneficiary, + amount: amount, + } + openCheques := mgr.openDebitCheques[beneficiary] + if openCheques == nil { + openCheques = make([]*Cheque, 0) + } + openCheques = append(openCheques, cheque) + mgr.openDebitCheques[beneficiary] = openCheques + return cheque +} diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 4846750c9b..9863ce20f4 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -36,6 +36,7 @@ const ( type SwapProtocol struct { peersMu sync.RWMutex peers map[discover.NodeID]*SwapProtocolPeer + swap *Swap } // Peer is the Peer extension for the streaming protocol @@ -44,9 +45,10 @@ type SwapProtocolPeer struct { swapProtocol *SwapProtocol } -func NewSwapProtocol() *SwapProtocol { +func NewSwapProtocol(swapAccount *Swap) *SwapProtocol { proto := &SwapProtocol{ peers: make(map[discover.NodeID]*SwapProtocolPeer), + swap: swapAccount, } return proto } @@ -61,6 +63,7 @@ func NewPeer(peer *protocols.Peer, swap *SwapProtocol) *SwapProtocolPeer { } type IssueChequeMsg struct { + Cheque *Cheque } type RedeemChequeMsg struct { diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 5235d4f596..3334ab9905 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -70,10 +70,11 @@ const ( // Swift Automatic Payments // a peer to peer micropayment system type Swap struct { - stateStore state.Store - lock sync.RWMutex - peers map[discover.NodeID]*SwapPeer - local *Params // local peer's swap parameters + chequeManager *ChequeManager + stateStore state.Store + lock sync.RWMutex + peers map[discover.NodeID]*SwapPeer + local *Params // local peer's swap parameters } type SwapPeer struct { @@ -244,14 +245,14 @@ func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, pric // -1 if sp.balance < payAt // 0 if sp.balance == payAt // +1 if sp.balance > payAt - if sp.balance.Cmp(payAt) > 1 { + if sp.balance.Cmp(payAt) == -1 { err := sp.issueCheque(ctx) if err != nil { //TODO: special error handling, as at this point the accounting has been done //but the cheque could not be sent? } } - if sp.balance.Cmp(dropAt) < 0 { + if sp.balance.Cmp(dropAt) == -1 { sp.Drop(ErrInsufficientFunds) } log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.ID(), sp.balance.String())) @@ -259,7 +260,11 @@ func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, pric } func (sp *SwapPeer) issueCheque(ctx context.Context) error { - msg := IssueChequeMsg{} + amount := big.NewInt(0) + cheque := sp.swapAccount.chequeManager.CreateCheque(sp.ID(), amount.Abs(payAt)) + msg := IssueChequeMsg{ + Cheque: cheque, + } return sp.Send(ctx, msg) } @@ -342,9 +347,10 @@ type PayProfile struct { func NewSwap(local *Params, stateStore state.Store) (swap *Swap, err error) { swap = &Swap{ - local: local, - stateStore: stateStore, - peers: make(map[discover.NodeID]*SwapPeer), + chequeManager: NewChequeManager(stateStore), + local: local, + stateStore: stateStore, + peers: make(map[discover.NodeID]*SwapPeer), } //swap.SetParams(local) diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 62cc9e17c9..99408d9092 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -26,6 +26,7 @@ import ( "path/filepath" "sync" "testing" + "time" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -55,6 +56,21 @@ var testSpec = &protocols.Spec{ }, } +type dummyRW struct{} + +func (d *dummyRW) WriteMsg(msg p2p.Msg) error { + return nil +} + +func (d *dummyRW) ReadMsg() (p2p.Msg, error) { + return p2p.Msg{ + Code: 0, + Size: 0, + Payload: nil, + ReceivedAt: time.Now(), + }, nil +} + type testExceedsPayAtMsg struct{} type testExceedsDropAtMsg struct{} @@ -81,107 +97,86 @@ func TestLimits(t *testing.T) { } } +//unit test for exceeds pay limit func TestExceedsPayAt(t *testing.T) { - dir, err := ioutil.TempDir("", "swap_test_store") - if err != nil { - panic(err) - } - defer os.RemoveAll(dir) - stateStore, err2 := state.NewDBStore(dir) - if err2 != nil { - panic(err2) - } - swap, err3 := NewSwap(NewDefaultSwapParams().Params, stateStore) - if err3 != nil { - t.Fatal(err3) - } - id := adapters.RandomNodeConfig().ID - testPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), nil, nil) + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + testPeer := newDummyPeer() sp := NewSwapPeer(testPeer, swap) - sp.handlerFunc = msgHandler + sp.handlerFunc = dummyMsgHandler + ctx := context.Background() - sp.handleAccountedMsg(ctx, &testExceedsPayAtMsg{}) + sp.Send(ctx, &testExceedsPayAtMsg{}) + + cheques := sp.swapAccount.chequeManager.openDebitCheques[sp.ID()] + if cheques == nil { + t.Fatal("Expected cheques for this peer to be present, but are nil") + } + if len(cheques) == 0 { + t.Fatal("Expected a cheque to have arrived, but len is zero") + } + if cheques[0].serial != 1 { + t.Fatal(fmt.Sprintf("Expected the serial to be one (first message but is: %d", cheques[0].serial)) + } + absPayAt := big.NewInt(0) + if cheques[0].amount.Cmp(absPayAt.Abs(payAt)) != 0 { + t.Fatal(fmt.Sprintf("Expected the serial to be one (first message but is: %d", cheques[0].serial)) + } } +//unit test for exceeds drop limit func TestExceedsDropAt(t *testing.T) { - dir, err := ioutil.TempDir("", "swap_test_store") - if err != nil { - panic(err) - } - defer os.RemoveAll(dir) - stateStore, err2 := state.NewDBStore(dir) - if err2 != nil { - panic(err2) - } - swap, err3 := NewSwap(NewDefaultSwapParams().Params, stateStore) - if err3 != nil { - t.Fatal(err3) - } - id := adapters.RandomNodeConfig().ID - testPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), nil, nil) + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + testPeer := newDummyPeer() sp := NewSwapPeer(testPeer, swap) - sp.handlerFunc = msgHandler + sp.handlerFunc = dummyMsgHandler + ctx := context.Background() - err = sp.Send(ctx, &testExceedsDropAtMsg{}) + err := sp.Send(ctx, &testExceedsDropAtMsg{}) if err != ErrInsufficientFunds { t.Fatal("Expected test to fail with insufficient funds, but it didn't") } } -func TestProtocol(t *testing.T) { - /* - dir, err := ioutil.TempDir("", "swap_test_store") - if err != nil { - panic(err) - } - defer os.RemoveAll(dir) - - swap := NewSwap(NewDefaultSwapParams().Params, state.NewDBStore(dir)) - conf := adapters.RandomNodeConfig() - - protocol := func(p *p2p.Peer, rw p2p.MsgReadWriter) error { - peer := protocols.NewPeer(p, rw, testSpec) - runProtocol(peer, swap) - } - protocolTester := p2ptest.NewProtocolTester(t, conf.ID, 2, protocol) - protocolTester.TestExchanges(p2p.TestExchange{ - Label: "", - Expects: []p2ptest.Expect{ - { - Code: 5, - Msg: &RetrieveRequestMsg{ - Addr: hash0[:], - SkipCheck: true, - }, - Peer: peerID, - }, - }, - }) - */ +func createTestSwap(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) + } + swap, err3 := NewSwap(NewDefaultSwapParams().Params, stateStore) + if err3 != nil { + t.Fatal(err3) + } + return swap, dir } func runProtocol(peer *protocols.Peer, swap *Swap) { sp := NewSwapPeer(peer, swap) - sp.Peer.Run(msgHandler) + sp.Peer.Run(dummyMsgHandler) } -func msgHandler(ctx context.Context, msg interface{}) error { +func dummyMsgHandler(ctx context.Context, msg interface{}) error { return nil } -func TestSwapProtocol(t *testing.T) { +func TestSwapRPC(t *testing.T) { + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) // create the two nodes stack_one, err := newServiceNode(p2pPort, 0, 0) if err != nil { - log.Crit("Create servicenode #1 fail", "err", err) - } - stack_two, err := newServiceNode(p2pPort+1, 0, 0) - if err != nil { - log.Crit("Create servicenode #2 fail", "err", err) + t.Fatal("Create servicenode #1 fail", "err", err) } - instance := NewSwapProtocol() + instance := NewSwapProtocol(swap) // wrapper function for servicenode to start the service swapsvc := func(ctx *node.ServiceContext) (node.Service, error) { return &API{ @@ -192,181 +187,80 @@ func TestSwapProtocol(t *testing.T) { // register adds the service to the services the servicenode starts when started err = stack_one.Register(swapsvc) if err != nil { - log.Crit("Register service in servicenode #1 fail", "err", err) + t.Fatal("Register service in servicenode #1 fail", "err", err) } - err = stack_two.Register(swapsvc) - if err != nil { - log.Crit("Register service in servicenode #2 fail", "err", err) - } - // start the nodes err = stack_one.Start() if err != nil { - log.Crit("servicenode #1 start failed", "err", err) - } - err = stack_two.Start() - if err != nil { - log.Crit("servicenode #2 start failed", "err", err) + t.Fatal("servicenode #1 start failed", "err", err) } // connect to the servicenode RPCs rpcclient_one, err := rpc.Dial(filepath.Join(stack_one.DataDir(), ipcpath)) if err != nil { - log.Crit("connect to servicenode #1 IPC fail", "err", err) + t.Fatal("connect to servicenode #1 IPC fail", "err", err) } defer os.RemoveAll(stack_one.DataDir()) - rpcclient_two, err := rpc.Dial(filepath.Join(stack_two.DataDir(), ipcpath)) - if err != nil { - log.Crit("connect to servicenode #2 IPC fail", "err", err) - } - defer os.RemoveAll(stack_two.DataDir()) - - // display that the initial pong counts are 0 - var balance int + var balance *big.Int err = rpcclient_one.Call(&balance, "swap_balance") if err != nil { - log.Crit("servicenode #1 pongcount RPC failed", "err", err) + t.Fatal("servicenode #1 RPC failed", "err", err) } - log.Info("servicenode #1 before ping", "balance-1", balance) + log.Debug("servicenode #1 balance", "balance-1", balance) - err = rpcclient_two.Call(&balance, "swap_balance") + if balance.Cmp(big.NewInt(0)) != 0 { + t.Fatal("Expected balance to be 0 but it is not") + } + + dummyPeer1 := newDummyPeer() + dummyPeer2 := newDummyPeer() + id1 := dummyPeer1.ID() + id2 := dummyPeer2.ID() + + fake1 := int64(234) + fake2 := int64(-100) + fakeBalance1 := big.NewInt(fake1) + fakeBalance2 := big.NewInt(fake2) + + swap.peers[id1] = NewSwapPeer(dummyPeer1, swap) + swap.peers[id2] = NewSwapPeer(dummyPeer2, swap) + swap.peers[id1].balance = fakeBalance1 + swap.peers[id2].balance = fakeBalance2 + + err = rpcclient_one.Call(&balance, "swap_balanceWithPeer", id1) if err != nil { - log.Crit("servicenode #2 pongcount RPC failed", "err", err) + t.Fatal("servicenode #1 RPC failed", "err", err) + } + log.Debug("balance1", "balance-1", balance) + if balance.Cmp(fakeBalance1) != 0 { + t.Fatal(fmt.Sprintf("Expected balance %s to be equal to fake balance %s, but it is not", balance.String(), fakeBalance1.String())) } - log.Info("servicenode #2 before ping", "balance-2", balance) - /* - // get the server instances - srv_one := stack_one.Server() - srv_two := stack_two.Server() + err = rpcclient_one.Call(&balance, "swap_balanceWithPeer", id2) + if err != nil { + t.Fatal("servicenode #1 RPC failed", "err", err) + } + log.Debug("balance2", "balance-2", balance) + if balance.Cmp(fakeBalance2) != 0 { + t.Fatal(fmt.Sprintf("Expected balance %s to be equal to fake balance %s, but it is not", balance.String(), fakeBalance2.String())) + } - // subscribe to peerevents - eventOneC := make(chan *p2p.PeerEvent) - sub_one := srv_one.SubscribeEvents(eventOneC) + err = rpcclient_one.Call(&balance, "swap_balance") + if err != nil { + t.Fatal("servicenode #1 RPC failed", "err", err) + } + log.Debug("balance", "balance", balance) - eventTwoC := make(chan *p2p.PeerEvent) - sub_two := srv_two.SubscribeEvents(eventTwoC) + fakeSum := big.NewInt(fake1 + fake2) + if balance.Cmp(fakeSum) != 0 { + t.Fatal(fmt.Sprintf("Expected balance %s to be equal to sum %s, but it is not", balance.String(), fakeSum.String())) + } +} - // connect the nodes - p2pnode_two := srv_two.Self() - srv_one.AddPeer(p2pnode_two) - - // fork and do the pinging - stackW.Add(2) - pingmax_one := 4 - pingmax_two := 2 - - go func() { - - // when we get the add event, we know we are connected - ev := <-eventOneC - if ev.Type != "add" { - log.Error("server #1 expected peer add", "eventtype", ev.Type) - stackW.Done() - return - } - log.Debug("server #1 connected", "peer", ev.Peer) - - // send the pings - for i := 0; i < pingmax_one; i++ { - err := rpcclient_one.Call(nil, "foo_ping", ev.Peer) - if err != nil { - log.Error("server #1 RPC ping fail", "err", err) - stackW.Done() - break - } - } - - // wait for all msgrecv events - // pings we receive, and pongs we expect from pings we sent - for i := 0; i < pingmax_two+pingmax_one; { - ev := <-eventOneC - log.Warn("msg", "type", ev.Type, "i", i) - if ev.Type == "msgrecv" { - i++ - } - } - - stackW.Done() - }() - - // mirrors the previous go func - go func() { - ev := <-eventTwoC - if ev.Type != "add" { - log.Error("expected peer add", "eventtype", ev.Type) - stackW.Done() - return - } - log.Debug("server #2 connected", "peer", ev.Peer) - for i := 0; i < pingmax_two; i++ { - err := rpcclient_two.Call(nil, "foo_ping", ev.Peer) - if err != nil { - log.Error("server #2 RPC ping fail", "err", err) - stackW.Done() - break - } - } - - for i := 0; i < pingmax_one+pingmax_two; { - ev := <-eventTwoC - if ev.Type == "msgrecv" { - log.Warn("msg", "type", ev.Type, "i", i) - i++ - } - } - - stackW.Done() - }() - - // wait for the two ping pong exchanges to finish - stackW.Wait() - - // tell the API to shut down - // this will disconnect the peers and close the channels connecting API and protocol - err = rpcclient_one.Call(nil, "foo_quit", srv_two.Self().ID) - if err != nil { - log.Error("server #1 RPC quit fail", "err", err) - } - err = rpcclient_two.Call(nil, "foo_quit", srv_one.Self().ID) - if err != nil { - log.Error("server #2 RPC quit fail", "err", err) - } - - // disconnect will generate drop events - for { - ev := <-eventOneC - if ev.Type == "drop" { - break - } - } - for { - ev := <-eventTwoC - if ev.Type == "drop" { - break - } - } - - // proudly inspect the results - err = rpcclient_one.Call(&count, "foo_pongCount") - if err != nil { - log.Crit("servicenode #1 pongcount RPC failed", "err", err) - } - log.Info("servicenode #1 after ping", "pongcount", count) - - err = rpcclient_two.Call(&count, "foo_pongCount") - if err != nil { - log.Crit("servicenode #2 pongcount RPC failed", "err", err) - } - log.Info("servicenode #2 after ping", "pongcount", count) - - // bring down the servicenodes - sub_one.Unsubscribe() - sub_two.Unsubscribe() - stack_one.Stop() - stack_two.Stop() - */ +func newDummyPeer() *protocols.Peer { + id := adapters.RandomNodeConfig().ID + return protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), &dummyRW{}, testSpec) } func newServiceNode(port int, httpport int, wsport int, modules ...string) (*node.Node, error) { From 370b5dabbe135243f79de26cc1103752f9386fb0 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Tue, 21 Aug 2018 17:25:33 -0500 Subject: [PATCH 09/30] swarm/swap: own network test file for swap; more tests and bug fixes --- swarm/network_test.go | 138 ----------------- swarm/swap/protocol.go | 1 - swarm/swap/swap.go | 22 +-- swarm/swap/swap_test.go | 56 +++++++ swarm/swap_test.go | 334 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 402 insertions(+), 149 deletions(-) create mode 100644 swarm/swap_test.go diff --git a/swarm/network_test.go b/swarm/network_test.go index 629095c936..4771676856 100644 --- a/swarm/network_test.go +++ b/swarm/network_test.go @@ -21,7 +21,6 @@ import ( "flag" "fmt" "io/ioutil" - "math/big" "math/rand" "os" "sync" @@ -377,143 +376,6 @@ func testSwarmNetwork(t *testing.T, o *testSwarmNetworkOptions, steps ...testSwa } } -func TestSwapNetworkSymmetricFileUpload(t *testing.T) { - nodeCount := 16 - - sim := simulation.New(map[string]simulation.ServiceFunc{ - "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { - config := api.NewConfig() - - dir, err := ioutil.TempDir("", "swap-network-test-node") - if err != nil { - return nil, nil, err - } - cleanup = func() { - err := os.RemoveAll(dir) - if err != nil { - log.Error("cleaning up swarm temp dir", "err", err) - } - } - - config.Path = dir - - privkey, err := crypto.GenerateKey() - if err != nil { - return nil, cleanup, err - } - - config.Init(privkey) - - swarm, err := NewSwarm(config, nil) - if err != nil { - return nil, cleanup, err - } - bucket.Store(bucketKeySwarm, swarm) - log.Info("new swarm", "bzzKey", config.BzzKey, "baseAddr", fmt.Sprintf("%x", swarm.bzz.BaseAddr())) - return swarm, cleanup, nil - }, - }) - defer sim.Close() - - ctx := context.Background() - files := make([]file, 0) - - var checkStatusM sync.Map - var nodeStatusM sync.Map - var totalFoundCount uint64 - - _, err := sim.AddNodesAndConnectChain(nodeCount) - if err != nil { - t.Fatal(err) - } - - result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { - nodeIDs := sim.UpNodeIDs() - shuffle(len(nodeIDs), func(i, j int) { - nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] - }) - for _, id := range nodeIDs { - key, data, err := uploadFile(sim.Service("swarm", id).(*Swarm)) - if err != nil { - return err - } - log.Trace("file uploaded", "node", id, "key", key.String()) - files = append(files, file{ - addr: key, - data: data, - nodeID: id, - }) - } - - if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { - return err - } - - // File retrieval check is repeated until all uploaded files are retrieved from all nodes - // or until the timeout is reached. - for { - if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 { - return nil - } - } - }) - - balancesMap := make(map[discover.NodeID]map[discover.NodeID]*big.Int) - - for _, node := range sim.NodeIDs() { - item, ok := sim.NodeItem(node, bucketKeySwarm) - if !ok { - log.Error("No swarm") - return - } - swarm := item.(*Swarm) - - subBalances := make(map[discover.NodeID]*big.Int) - - for _, n := range sim.NodeIDs() { - if node == n { - continue - } - balance := swarm.swap.GetPeerBalance(n) - if balance != nil { - subBalances[n] = balance - log.Debug(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) - } else { - log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) - } - } - balancesMap[node] = subBalances - } - - if *printStats { - for k, v := range balancesMap { - fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) - for kk, vv := range v { - fmt.Println(fmt.Sprintf(".........with node %s: balance %s", kk.TerminalString(), vv.String())) - } - } - } - - for k, mapForK := range balancesMap { - for n, balanceKwithN := range mapForK { - for subK, mapForSubK := range balancesMap { - if n == subK { - log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) - log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) - if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { - log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") - } - } - } - } - } - - if result.Error != nil { - t.Fatal(result.Error) - } - log.Debug("test terminated") -} - // uploadFile, uploads a short file to the swarm instance // using the api.Put method. func uploadFile(swarm *Swarm) (storage.Address, string, error) { diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 9863ce20f4..78517e82e4 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -187,7 +187,6 @@ func (p *SwapProtocolPeer) handleSwapMsg(ctx context.Context, msg interface{}) e default: return fmt.Errorf("unknown message type: %T", msg) } - return nil } func (sp *SwapProtocolPeer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 3334ab9905..f0f27443e1 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -191,16 +191,17 @@ func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, di price := accounted.GetMsgPrice() //local node is being credited (in its favor), so check upper limit if direction == CreditEntry { - //TODO: is there are a check needed here? + //TODO: is there a check needed here? //It should actually have been done on the client side, the debitor! //creditor could theoretically go over payAt, but if well done, //should have been checked on the client side so this shouldn't happen? - checkBalance := sp.balance.Add(sp.balance, price) - //(checkBalance *Int) Cmp(payAt) - // -1 if checkBalance < payAt - // 0 if checkBalance == payAt - // +1 if checkBalance > payAt - if checkBalance.Cmp(payAt) == 1 { + checkBalance := &big.Int{} + checkBalance.Add(sp.balance, price) + //(checkBalance *Int) CmpAbs(payAt) + // -1 if |checkBalance| < |payAt| + // 0 if |checkBalance| == |payAt| + // +1 if |checkBalance| > |payAt| + if checkBalance.CmpAbs(payAt) == 1 { return nil, ErrInsufficientFunds } } else if direction == DebitEntry { @@ -213,7 +214,8 @@ func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, di // -1 if checkBalance < dropAt // 0 if checkBalance == dropAt // +1 if checkBalance > dropAt - checkBalance := sp.balance.Sub(sp.balance, price.Abs(price)) + checkBalance := &big.Int{} + checkBalance.Sub(sp.balance, price.Abs(price)) if checkBalance.Cmp(dropAt) == -1 { return nil, ErrInsufficientFunds } @@ -234,10 +236,10 @@ func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, pric if direction == CreditEntry { //NOTE: do we need to check for sufficient funds again? //operations are not atomic/transactional, so balance may have changed in the meanwhile! - sp.balance = sp.balance.Add(sp.balance, price) + sp.balance.Add(sp.balance, price) //local node is being debited (in favor of remote peer), so its balance decreases } else if direction == DebitEntry { - sp.balance = sp.balance.Sub(sp.balance, price) + sp.balance.Sub(sp.balance, price) } //TODO: save to store here? init store? sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 99408d9092..403716125f 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -53,6 +53,7 @@ var testSpec = &protocols.Spec{ Messages: []interface{}{ testExceedsPayAtMsg{}, testExceedsDropAtMsg{}, + testCheapMsg{}, }, } @@ -73,6 +74,7 @@ func (d *dummyRW) ReadMsg() (p2p.Msg, error) { type testExceedsPayAtMsg struct{} type testExceedsDropAtMsg struct{} +type testCheapMsg struct{} func (tmsg *testExceedsPayAtMsg) GetMsgPrice() *big.Int { diff := &big.Int{} @@ -84,6 +86,10 @@ func (tmsg *testExceedsDropAtMsg) GetMsgPrice() *big.Int { return diff.Sub(dropAt, big.NewInt(1)) } +func (tmsg *testCheapMsg) GetMsgPrice() *big.Int { + return big.NewInt(100) +} + func init() { flag.Parse() @@ -141,6 +147,56 @@ func TestExceedsDropAt(t *testing.T) { } } +func TestSendCheapMessage(t *testing.T) { + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + testPeer := newDummyPeer() + sp := NewSwapPeer(testPeer, swap) + testBalance := big.NewInt(1234567890) + sp.balance = testBalance + + msg := &testCheapMsg{} + ctx := context.Background() + err := sp.Send(ctx, msg) + if err != nil { + t.Fatal("Unexpected error sending message") + } + + if sp.balance.Cmp(testBalance.Sub(testBalance, msg.GetMsgPrice())) != 0 { + t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", + testBalance.Sub(testBalance, msg.GetMsgPrice()).String(), sp.balance.String())) + } +} + +func TestRestoreBalanceFromStateStore(t *testing.T) { + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + testPeer := newDummyPeer() + sp := NewSwapPeer(testPeer, swap) + testBalance := big.NewInt(1234567890) + sp.balance = big.NewInt(1234567890) + + //send a message, should trigger saving to stateStore + msg := &testCheapMsg{} + ctx := context.Background() + err := sp.Send(ctx, msg) + if err != nil { + log.Error(err.Error()) + t.Fatal("Unexpected error sending message") + } + + sp2 := NewSwapPeer(testPeer, swap) + + expectedBalance := &big.Int{} + expectedBalance.Sub(testBalance, msg.GetMsgPrice()) + if sp2.balance.Cmp(expectedBalance) != 0 { + t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", + expectedBalance.String(), sp2.balance.String())) + } +} + func createTestSwap(t *testing.T) (*Swap, string) { dir, err := ioutil.TempDir("", "swap_test_store") if err != nil { diff --git a/swarm/swap_test.go b/swarm/swap_test.go new file mode 100644 index 0000000000..dfca84197f --- /dev/null +++ b/swarm/swap_test.go @@ -0,0 +1,334 @@ +package swarm + +import ( + "context" + "fmt" + "io/ioutil" + "math/big" + "math/rand" + "os" + "strconv" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/simulations/adapters" + "github.com/ethereum/go-ethereum/swarm/api" + "github.com/ethereum/go-ethereum/swarm/log" + "github.com/ethereum/go-ethereum/swarm/network/simulation" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +func TestSwapNetworkSymmetricFileUpload(t *testing.T) { + nodeCount := 16 + + sim := simulation.New(map[string]simulation.ServiceFunc{ + "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { + config := api.NewConfig() + + dir, err := ioutil.TempDir("", "swap-network-test-node") + if err != nil { + return nil, nil, err + } + cleanup = func() { + err := os.RemoveAll(dir) + if err != nil { + log.Error("cleaning up swarm temp dir", "err", err) + } + } + + config.Path = dir + + privkey, err := crypto.GenerateKey() + if err != nil { + return nil, cleanup, err + } + + config.Init(privkey) + + swarm, err := NewSwarm(config, nil) + if err != nil { + return nil, cleanup, err + } + bucket.Store(bucketKeySwarm, swarm) + log.Info("new swarm", "bzzKey", config.BzzKey, "baseAddr", fmt.Sprintf("%x", swarm.bzz.BaseAddr())) + return swarm, cleanup, nil + }, + }) + defer sim.Close() + + ctx := context.Background() + files := make([]file, 0) + + var checkStatusM sync.Map + var nodeStatusM sync.Map + var totalFoundCount uint64 + + _, err := sim.AddNodesAndConnectChain(nodeCount) + if err != nil { + t.Fatal(err) + } + + result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { + nodeIDs := sim.UpNodeIDs() + shuffle(len(nodeIDs), func(i, j int) { + nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] + }) + for _, id := range nodeIDs { + key, data, err := uploadFile(sim.Service("swarm", id).(*Swarm)) + if err != nil { + return err + } + log.Trace("file uploaded", "node", id, "key", key.String()) + files = append(files, file{ + addr: key, + data: data, + nodeID: id, + }) + } + + if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { + return err + } + + // File retrieval check is repeated until all uploaded files are retrieved from all nodes + // or until the timeout is reached. + for { + if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 { + return nil + } + } + }) + + balancesMap := make(map[discover.NodeID]map[discover.NodeID]*big.Int) + + for _, node := range sim.NodeIDs() { + item, ok := sim.NodeItem(node, bucketKeySwarm) + if !ok { + log.Error("No swarm") + return + } + swarm := item.(*Swarm) + + subBalances := make(map[discover.NodeID]*big.Int) + + for _, n := range sim.NodeIDs() { + if node == n { + continue + } + balance := swarm.swap.GetPeerBalance(n) + if balance != nil { + subBalances[n] = balance + log.Debug(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) + } else { + log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + } + } + balancesMap[node] = subBalances + } + + if *printStats { + for k, v := range balancesMap { + fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) + for kk, vv := range v { + fmt.Println(fmt.Sprintf(".........with node %s: balance %s", kk.TerminalString(), vv.String())) + } + } + } + + for k, mapForK := range balancesMap { + for n, balanceKwithN := range mapForK { + for subK, mapForSubK := range balancesMap { + if n == subK { + log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) + log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) + if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { + log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + } + } + } + } + } + + if result.Error != nil { + t.Fatal(result.Error) + } + log.Debug("test terminated") +} + +func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { + nodeCount := 16 + + sim := simulation.New(map[string]simulation.ServiceFunc{ + "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { + config := api.NewConfig() + + dir, err := ioutil.TempDir("", "swap-network-test-node") + if err != nil { + return nil, nil, err + } + cleanup = func() { + err := os.RemoveAll(dir) + if err != nil { + log.Error("cleaning up swarm temp dir", "err", err) + } + } + + config.Path = dir + + privkey, err := crypto.GenerateKey() + if err != nil { + return nil, cleanup, err + } + + config.Init(privkey) + + swarm, err := NewSwarm(config, nil) + if err != nil { + return nil, cleanup, err + } + bucket.Store(bucketKeySwarm, swarm) + log.Info("new swarm", "bzzKey", config.BzzKey, "baseAddr", fmt.Sprintf("%x", swarm.bzz.BaseAddr())) + return swarm, cleanup, nil + }, + }) + defer sim.Close() + + ctx := context.Background() + files := make([]file, 0) + + var checkStatusM sync.Map + var nodeStatusM sync.Map + var totalFoundCount uint64 + + _, err := sim.AddNodesAndConnectChain(nodeCount) + if err != nil { + t.Fatal(err) + } + + const maxFileSize = 1024 * 1024 * 4 //1024 bytes * 1024 * 4 = 4MB + const minfileSize = 1024 + + //pseudo random algo to define if a node will upload or not + //if a bit is 0, do not upload + pseudoRandomNum := rand.Int63() + pseudoRandomBitMask := strconv.FormatInt(pseudoRandomNum, 2) + + result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { + nodeIDs := sim.UpNodeIDs() + shuffle(len(nodeIDs), func(i, j int) { + nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] + }) + for i, id := range nodeIDs { + //if the position in random num is 0, don't upload + if string(pseudoRandomBitMask[i]) != "0" { + size := rand.Intn(maxFileSize-minfileSize) + minfileSize + key, data, err := uploadRandomFileSize(sim.Service("swarm", id).(*Swarm), size) + if err != nil { + return err + } + log.Trace("file uploaded", "node", id, "key", key.String()) + files = append(files, file{ + addr: key, + data: data, + nodeID: id, + }) + } + } + + if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { + return err + } + + // File retrieval check is repeated until all uploaded files are retrieved from all nodes + // or until the timeout is reached. + for { + if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 { + return nil + } + } + }) + + balancesMap := make(map[discover.NodeID]map[discover.NodeID]*big.Int) + + for _, node := range sim.NodeIDs() { + item, ok := sim.NodeItem(node, bucketKeySwarm) + if !ok { + log.Error("No swarm") + return + } + swarm := item.(*Swarm) + + subBalances := make(map[discover.NodeID]*big.Int) + + for _, n := range sim.NodeIDs() { + if node == n { + continue + } + balance := swarm.swap.GetPeerBalance(n) + if balance != nil { + subBalances[n] = balance + log.Debug(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) + } else { + log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + } + } + balancesMap[node] = subBalances + } + + if *printStats { + for k, v := range balancesMap { + fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) + for kk, vv := range v { + fmt.Println(fmt.Sprintf(".........with node %s: balance %s", kk.TerminalString(), vv.String())) + } + } + } + + /* + TODO: Should balances in this case also be symmetric? + + for k, mapForK := range balancesMap { + for n, balanceKwithN := range mapForK { + for subK, mapForSubK := range balancesMap { + if n == subK { + log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) + log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) + if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { + log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + } + } + } + } + } + */ + + if result.Error != nil { + t.Fatal(result.Error) + } + log.Debug("test terminated") +} + +// uploadFile, uploads a short file to the swarm instance +// using the api.Put method. +func uploadRandomFileSize(swarm *Swarm, size int) (storage.Address, string, error) { + b := make([]byte, size) + _, err := rand.Read(b) + if err != nil { + return nil, "", err + } + // uniqueness is very certain. + data := fmt.Sprintf("test content %s %x", time.Now().Round(0), b) + ctx := context.TODO() + k, wait, err := swarm.api.Put(ctx, data, "text/plain", false) + if err != nil { + return nil, "", err + } + if wait != nil { + err = wait(ctx) + } + return k, data, err +} From 9c024e2517b6d8d066e044155c6e37e2d096afd2 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Mon, 27 Aug 2018 18:58:55 -0500 Subject: [PATCH 10/30] swarm/swap: addressed first review comments --- swarm/swap/api.go | 23 +-- swarm/swap/protocol.go | 80 ++++++---- swarm/swap/swap.go | 346 +++------------------------------------- swarm/swap/swap_test.go | 76 +++++++-- 4 files changed, 141 insertions(+), 384 deletions(-) diff --git a/swarm/swap/api.go b/swarm/swap/api.go index 960530e870..c224d43f2f 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -21,7 +21,6 @@ import ( "errors" "math/big" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/p2p/discover" ) @@ -29,25 +28,22 @@ var ( ErrNoSuchPeerAccounting = errors.New("No accounting with that peer") ) -// Wrapper for receiving pss messages when using the pss API -// providing access to sender of message -type APIMsg struct { - Msg hexutil.Bytes -} - -// Additional public methods accessible through API for pss +//This is the API definition to access swarm swap accounting data via RPC type API struct { - *SwapProtocol + *Protocol } //TODO: define metrics +//Get metrics about swap for this node type SwapMetrics struct { } -func NewAPI(swap *SwapProtocol) *API { - return &API{SwapProtocol: swap} +//Create a new API instance +func NewAPI(swap *Protocol) *API { + return &API{Protocol: swap} } +//Get the balance for this node with a specific peer func (swapapi *API) BalanceWithPeer(ctx context.Context, peer discover.NodeID) (balance *big.Int, err error) { balance = swapapi.swap.peers[peer].balance if balance == nil { @@ -56,6 +52,10 @@ func (swapapi *API) BalanceWithPeer(ctx context.Context, peer discover.NodeID) ( return } +//Get the overall balance of the node +//Iterates over all peers this node is having accounted interaction +//and just adds up balances. +//It assumes that if a disfavorable balance is represented as a negative value func (swapapi *API) Balance(ctx context.Context) (balance *big.Int, err error) { balance = big.NewInt(0) for _, peer := range swapapi.swap.peers { @@ -64,6 +64,7 @@ func (swapapi *API) Balance(ctx context.Context) (balance *big.Int, err error) { return } +//Just return the Swap metrics func (swapapi *API) GetSwapMetrics() (*SwapMetrics, error) { return nil, nil } diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 78517e82e4..0ee3fc575d 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -33,52 +33,63 @@ const ( IsActiveProtocol = true ) -type SwapProtocol struct { +//Here we define the Swap p2p.protocol +//We use it to standardize the interaction between nodes who need clear their accounts. +//Accounting itself is separate (see swarm/swap/swap.go) +//This protocol focuses on sending and receiving cheques and +//cheque management/clearing +//For a better understanding, please read the Swap network paper "Generalized Swap swear and swindle games" +type Protocol struct { peersMu sync.RWMutex - peers map[discover.NodeID]*SwapProtocolPeer + peers map[discover.NodeID]*Peer swap *Swap } -// Peer is the Peer extension for the streaming protocol -type SwapProtocolPeer struct { +//This is the peer representing a participant in this protocol +type Peer struct { *protocols.Peer - swapProtocol *SwapProtocol + swapProtocol *Protocol } -func NewSwapProtocol(swapAccount *Swap) *SwapProtocol { - proto := &SwapProtocol{ - peers: make(map[discover.NodeID]*SwapProtocolPeer), +//Create a new protocol instance +func NewSwapProtocol(swapAccount *Swap) *Protocol { + proto := &Protocol{ + peers: make(map[discover.NodeID]*Peer), swap: swapAccount, } return proto } -// NewPeer is the constructor for Peer -func NewPeer(peer *protocols.Peer, swap *SwapProtocol) *SwapProtocolPeer { - p := &SwapProtocolPeer{ +// NewPeer is the constructor for the protocol Peer +func NewPeer(peer *protocols.Peer, swap *Protocol) *Peer { + p := &Peer{ Peer: peer, swapProtocol: swap, } return p } +//In a peer exchange, if node A gets too indebted with node B, +//node A issues a cheque and sends it to B type IssueChequeMsg struct { Cheque *Cheque } +//In a scenario where B received a cheque from A, node B can +//redeem a cheque, which means it kicks off the process to cash it in. +//In this case, this message is sent to peer A type RedeemChequeMsg struct { } ///////////////////////////////////////////////////////////////////// // SECTION: node.Service interface ///////////////////////////////////////////////////////////////////// - -func (p *SwapProtocol) Start(srv *p2p.Server) error { +func (p *Protocol) Start(srv *p2p.Server) error { log.Debug("Started swap") return nil } -func (p *SwapProtocol) Stop() error { +func (p *Protocol) Stop() error { log.Info("swap shutting down") return nil } @@ -88,11 +99,12 @@ var swapSpec = &protocols.Spec{ Version: swapVersion, MaxMsgSize: defaultMaxMsgSize, Messages: []interface{}{ - SwapMsg{}, + IssueChequeMsg{}, + RedeemChequeMsg{}, }, } -func (p *SwapProtocol) Protocols() []p2p.Protocol { +func (p *Protocol) Protocols() []p2p.Protocol { return []p2p.Protocol{ { Name: swapSpec.Name, @@ -103,7 +115,7 @@ func (p *SwapProtocol) Protocols() []p2p.Protocol { } } -func (p *SwapProtocol) APIs() []rpc.API { +func (p *Protocol) APIs() []rpc.API { apis := []rpc.API{ { Namespace: "swap", @@ -118,7 +130,7 @@ func (p *SwapProtocol) APIs() []rpc.API { ///////////////////////////////////////////////////////////////////// // SECTION: p2p.protocol interface ///////////////////////////////////////////////////////////////////// -func (swap *SwapProtocol) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { +func (swap *Protocol) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { p := protocols.NewPeer(peer, rw, swapSpec) sp := NewPeer(p, swap) swap.setPeer(sp) @@ -127,50 +139,51 @@ func (swap *SwapProtocol) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { return sp.Run(sp.handleSwapMsg) } -func (swap *SwapProtocol) NodeInfo() interface{} { +func (swap *Protocol) NodeInfo() interface{} { return nil } -func (swap *SwapProtocol) PeerInfo(id discover.NodeID) interface{} { +func (swap *Protocol) PeerInfo(id discover.NodeID) interface{} { return nil } //------------------------------------------------------------------------------------------ -func (swap *SwapProtocol) Close() { +func (swap *Protocol) Close() { } -func (swap *SwapProtocol) getPeer(peerId discover.NodeID) *SwapProtocolPeer { +func (swap *Protocol) getPeer(peerId discover.NodeID) *Peer { swap.peersMu.RLock() defer swap.peersMu.RUnlock() return swap.peers[peerId] } -func (swap *SwapProtocol) setPeer(peer *SwapProtocolPeer) { +func (swap *Protocol) setPeer(peer *Peer) { swap.peersMu.Lock() defer swap.peersMu.Unlock() swap.peers[peer.ID()] = peer - metrics.GetOrRegisterGauge("registry.peers", nil).Update(int64(len(swap.peers))) + metrics.GetOrRegisterGauge("swap.peers", nil).Update(int64(len(swap.peers))) } -func (swap *SwapProtocol) deletePeer(peer *SwapProtocolPeer) { +func (swap *Protocol) deletePeer(peer *Peer) { swap.peersMu.Lock() defer swap.peersMu.Unlock() delete(swap.peers, peer.ID()) - metrics.GetOrRegisterGauge("registry.peers", nil).Update(int64(len(swap.peers))) + metrics.GetOrRegisterGauge("swap.peers", nil).Update(int64(len(swap.peers))) swap.peersMu.Unlock() } -func (swap *SwapProtocol) peersCount() (c int) { +func (swap *Protocol) peersCount() (c int) { swap.peersMu.Lock() c = len(swap.peers) swap.peersMu.Unlock() return } -func (p *SwapProtocolPeer) handleSwapMsg(ctx context.Context, msg interface{}) error { +//Protocol message handler for handling cheque messages +func (p *Peer) handleSwapMsg(ctx context.Context, msg interface{}) error { switch msg := msg.(type) { case *IssueChequeMsg: @@ -179,22 +192,19 @@ func (p *SwapProtocolPeer) handleSwapMsg(ctx context.Context, msg interface{}) e case *RedeemChequeMsg: return p.handleRedeemChequeMsg(ctx, msg) - /* - case *QuitMsg: - return p.handleQuitMsg(msg) - */ - default: return fmt.Errorf("unknown message type: %T", msg) } } -func (sp *SwapProtocolPeer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { +//A IssueChequeMsg has been received +func (sp *Peer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { log.Debug("SwapProtocolPeer: handleIssueChequeMsg") return err } -func (sp *SwapProtocolPeer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { +//A RedeemChequeMsg has been received +func (sp *Peer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { log.Debug("SwapProtocolPeer: handleRedeemChequeMsg") return err } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index f0f27443e1..c31c528906 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -18,26 +18,16 @@ package swap import ( "context" - "crypto/ecdsa" "errors" "fmt" "math/big" - "os" - "path/filepath" "sync" "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/contracts/chequebook" - "github.com/ethereum/go-ethereum/contracts/chequebook/contract" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/state" - whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" ) const ( @@ -47,15 +37,8 @@ const ( ) var ( - autoCashInterval = 300 * time.Second // default interval for autocash - autoCashThreshold = big.NewInt(50000000000000) // threshold that triggers autocash (wei) - autoDepositInterval = 300 * time.Second // default interval for autocash - autoDepositThreshold = big.NewInt(50000000000000) // threshold that triggers autodeposit (wei) - autoDepositBuffer = big.NewInt(100000000000000) // buffer that is surplus for fork protection etc (wei) - buyAt = big.NewInt(20000000000) // maximum chunk price host is willing to pay (wei) - sellAt = big.NewInt(20000000000) // minimum chunk price host requires (wei) - payAt = big.NewInt(-4096 * 10000) // threshold that triggers payment {request} (bytes) - dropAt = big.NewInt(-4096 * 12000) // threshold that triggers disconnect (bytes) + payAt = big.NewInt(-4096 * 10000) // threshold that triggers payment {request} (bytes) + dropAt = big.NewInt(-4096 * 12000) // threshold that triggers disconnect (bytes) ErrInsufficientFunds = errors.New("Insufficient funds") ErrNotAccountedMsg = errors.New("Message does not need accounting") @@ -66,17 +49,19 @@ const ( chequebookDeployDelay = 1 * time.Second // delay between retries ) -// SwAP Swarm Accounting Protocol with -// Swift Automatic Payments +// SwAP Swarm Accounting Protocol // a peer to peer micropayment system +// A node maintains an individual balance with every peer +// Only messages which have a price will be accounted for type Swap struct { chequeManager *ChequeManager stateStore state.Store lock sync.RWMutex peers map[discover.NodeID]*SwapPeer - local *Params // local peer's swap parameters } +//Protocols which want to send and handle priced messages will need to use +//this peer instead of protocols.Peer, which is embedded type SwapPeer struct { *protocols.Peer lock sync.RWMutex @@ -86,6 +71,7 @@ type SwapPeer struct { storeID string } +//This defines if a price will be debited or credited to an account type EntryDirection bool const ( @@ -93,8 +79,9 @@ const ( CreditEntry EntryDirection = false ) -type SwapAccountedMsgType interface { - GetMsgPrice() *big.Int +//A message which needs accounting needs to implement this interface +type PricedMsg interface { + Price() *big.Int } //Handler for received messages @@ -104,7 +91,7 @@ func (sp *SwapPeer) RunAccountedProtocol(protocolHandler func(ctx context.Contex sp.handlerFunc = protocolHandler //then run the handler loop function - return sp.Run(sp.handleAccountedMsg) + return sp.Run(sp.handle) } //get a peer's balance @@ -118,12 +105,12 @@ func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { //Handle a received message; this is the handler loop function. //Check if it needs accounting, and if yes, apply accounting logic: //Check for sufficient funds, perform operation, then account -func (sp *SwapPeer) handleAccountedMsg(ctx context.Context, msg interface{}) error { +func (sp *SwapPeer) handle(ctx context.Context, msg interface{}) error { var err error var price *big.Int //the message is one which needs accounting... - if _, ok := msg.(SwapAccountedMsgType); ok { + if _, ok := msg.(PricedMsg); ok { //..so first check if there are enough funds for the operation available //(for crediting, this means if we are not essentially "overdrafting", or crossing the threshold) price, err = sp.checkAvailableFunds(ctx, msg, CreditEntry) @@ -157,7 +144,7 @@ func (sp *SwapPeer) Send(ctx context.Context, msg interface{}) error { var price *big.Int //the message is one which needs accounting... - if _, ok := msg.(SwapAccountedMsgType); ok { + if _, ok := msg.(PricedMsg); ok { //..so first check if there are enough funds for the operation available price, err = sp.checkAvailableFunds(ctx, msg, DebitEntry) //if not (or some other error occured), return error @@ -187,8 +174,8 @@ func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, di sp.lock.Lock() defer sp.lock.Unlock() - if accounted, ok := msg.(SwapAccountedMsgType); ok { - price := accounted.GetMsgPrice() + if accounted, ok := msg.(PricedMsg); ok { + price := accounted.Price() //local node is being credited (in its favor), so check upper limit if direction == CreditEntry { //TODO: is there a check needed here? @@ -229,7 +216,7 @@ func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, di //Thus, we credit the balance and increase it when the amount is in favor of the local node //We debit the balance and decrease it when the amount is in favor of the remote peer func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, price *big.Int, direction EntryDirection) { - if _, ok := msg.(SwapAccountedMsgType); ok { + if _, ok := msg.(PricedMsg); ok { sp.lock.Lock() defer sp.lock.Unlock() //local node is being credited (in its favor), so its balance increases @@ -252,6 +239,7 @@ func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, pric if err != nil { //TODO: special error handling, as at this point the accounting has been done //but the cheque could not be sent? + log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) } } if sp.balance.Cmp(dropAt) == -1 { @@ -261,12 +249,15 @@ func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, pric } } +//Issue a cheque for the remote peer. Happens if we are indebted with the peer +//and crossed the payment threshold func (sp *SwapPeer) issueCheque(ctx context.Context) error { - amount := big.NewInt(0) + amount := &big.Int{} cheque := sp.swapAccount.chequeManager.CreateCheque(sp.ID(), amount.Abs(payAt)) msg := IssueChequeMsg{ Cheque: cheque, } + //TODO: This should now be via the actual SwapProtocol return sp.Send(ctx, msg) } @@ -287,303 +278,14 @@ func NewSwapPeer(peer *protocols.Peer, swap *Swap) *SwapPeer { return sp } -// Profile - public swap profile -// public parameters for SWAP, serializable config struct passed in handshake -type Profile struct { - BuyAt *big.Int // accepted max price for chunk - SellAt *big.Int // offered sale price for chunk - PayAt *big.Int // threshold that triggers payment request - DropAt *big.Int // threshold that triggers disconnect -} - -// Strategy encapsulates parameters relating to -// automatic deposit and automatic cashing -type Strategy struct { - AutoCashInterval time.Duration // default interval for autocash - AutoCashThreshold *big.Int // threshold that triggers autocash (wei) - AutoDepositInterval time.Duration // default interval for autocash - AutoDepositThreshold *big.Int // threshold that triggers autodeposit (wei) - AutoDepositBuffer *big.Int // buffer that is surplus for fork protection etc (wei) -} - -// SwapMsg encapsulates messages transported over pss. -type SwapMsg struct { - To []byte - Control []byte - Expire uint32 - Payload *whisper.Envelope -} - -// Params extends the public profile with private parameters relating to -// automatic deposit and automatic cashing -type Params struct { - *Profile - *Strategy -} - -// LocalProfile combines a PayProfile with *swap.Params -type LocalProfile struct { - *Params - *PayProfile -} - -// RemoteProfile combines a PayProfile with *swap.Profile -type RemoteProfile struct { - *Profile - *PayProfile -} - -// PayProfile is a container for relevant chequebook and beneficiary options -type PayProfile struct { - PublicKey string // check against signature of promise - Contract common.Address // address of chequebook contract - Beneficiary common.Address // recipient address for swarm sales revenue - privateKey *ecdsa.PrivateKey - publicKey *ecdsa.PublicKey - owner common.Address - chbook *chequebook.Chequebook - lock sync.RWMutex -} - // New - swap constructor -func NewSwap(local *Params, stateStore state.Store) (swap *Swap, err error) { +func New(stateStore state.Store) (swap *Swap, err error) { swap = &Swap{ chequeManager: NewChequeManager(stateStore), - local: local, stateStore: stateStore, peers: make(map[discover.NodeID]*SwapPeer), } - //swap.SetParams(local) return } - -// NewDefaultSwapParams create params with default values -func NewDefaultSwapParams() *LocalProfile { - return &LocalProfile{ - PayProfile: &PayProfile{}, - Params: &Params{ - Profile: &Profile{ - BuyAt: buyAt, - SellAt: sellAt, - PayAt: payAt, - DropAt: dropAt, - }, - Strategy: &Strategy{ - AutoCashInterval: autoCashInterval, - AutoCashThreshold: autoCashThreshold, - AutoDepositInterval: autoDepositInterval, - AutoDepositThreshold: autoDepositThreshold, - AutoDepositBuffer: autoDepositBuffer, - }, - }, - } -} - -// Init this can only finally be set after all config options (file, cmd line, env vars) -// have been evaluated -func (lp *LocalProfile) Init(contract common.Address, prvkey *ecdsa.PrivateKey) { - pubkey := &prvkey.PublicKey - - lp.PayProfile = &PayProfile{ - PublicKey: common.ToHex(crypto.FromECDSAPub(pubkey)), - Contract: contract, - Beneficiary: crypto.PubkeyToAddress(*pubkey), - privateKey: prvkey, - publicKey: pubkey, - owner: crypto.PubkeyToAddress(*pubkey), - } -} - -// Chequebook get's chequebook from the localProfile -func (lp *LocalProfile) Chequebook() *chequebook.Chequebook { - defer lp.lock.Unlock() - lp.lock.Lock() - return lp.chbook -} - -// PrivateKey accessor -func (lp *LocalProfile) PrivateKey() *ecdsa.PrivateKey { - return lp.privateKey -} - -// func (self *LocalProfile) PublicKey() *ecdsa.PublicKey { -// return self.publicKey -// } - -// SetKey set's private and public key on localProfile -func (lp *LocalProfile) SetKey(prvkey *ecdsa.PrivateKey) { - lp.privateKey = prvkey - lp.publicKey = &prvkey.PublicKey -} - -// SetChequebook wraps the chequebook initialiser and sets up autoDeposit to cover spending. -func (lp *LocalProfile) SetChequebook(ctx context.Context, backend chequebook.Backend, path string) error { - lp.lock.Lock() - swapContract := lp.Contract - lp.lock.Unlock() - - valid, err := chequebook.ValidateCode(ctx, backend, swapContract) - if err != nil { - return err - } else if valid { - return lp.newChequebookFromContract(path, backend) - } - return lp.deployChequebook(ctx, backend, path) -} - -// deployChequebook deploys the localProfile Chequebook -func (lp *LocalProfile) deployChequebook(ctx context.Context, backend chequebook.Backend, path string) error { - opts := bind.NewKeyedTransactor(lp.privateKey) - opts.Value = lp.AutoDepositBuffer - opts.Context = ctx - - log.Info(fmt.Sprintf("Deploying new chequebook (owner: %v)", opts.From.Hex())) - address, err := deployChequebookLoop(opts, backend) - if err != nil { - log.Error(fmt.Sprintf("unable to deploy new chequebook: %v", err)) - return err - } - log.Info(fmt.Sprintf("new chequebook deployed at %v (owner: %v)", address.Hex(), opts.From.Hex())) - - // need to save config at this point - lp.lock.Lock() - lp.Contract = address - err = lp.newChequebookFromContract(path, backend) - lp.lock.Unlock() - if err != nil { - log.Warn(fmt.Sprintf("error initialising cheque book (owner: %v): %v", opts.From.Hex(), err)) - } - return err -} - -// deployChequebookLoop repeatedly tries to deploy a chequebook. -func deployChequebookLoop(opts *bind.TransactOpts, backend chequebook.Backend) (addr common.Address, err error) { - var tx *types.Transaction - for try := 0; try < chequebookDeployRetries; try++ { - if try > 0 { - time.Sleep(chequebookDeployDelay) - } - if _, tx, _, err = contract.DeployChequebook(opts, backend); err != nil { - log.Warn(fmt.Sprintf("can't send chequebook deploy tx (try %d): %v", try, err)) - continue - } - if addr, err = bind.WaitDeployed(opts.Context, backend, tx); err != nil { - log.Warn(fmt.Sprintf("chequebook deploy error (try %d): %v", try, err)) - continue - } - return addr, nil - } - return addr, err -} - -// newChequebookFromContract - initialise the chequebook from a persisted json file or create a new one -// caller holds the lock -func (lp *LocalProfile) newChequebookFromContract(path string, backend chequebook.Backend) error { - hexkey := common.Bytes2Hex(lp.Contract.Bytes()) - err := os.MkdirAll(filepath.Join(path, "chequebooks"), os.ModePerm) - if err != nil { - return fmt.Errorf("unable to create directory for chequebooks: %v", err) - } - - chbookpath := filepath.Join(path, "chequebooks", hexkey+".json") - lp.chbook, err = chequebook.LoadChequebook(chbookpath, lp.privateKey, backend, true) - - if err != nil { - lp.chbook, err = chequebook.NewChequebook(chbookpath, lp.Contract, lp.privateKey, backend) - if err != nil { - log.Warn(fmt.Sprintf("unable to initialise chequebook (owner: %v): %v", lp.owner.Hex(), err)) - return fmt.Errorf("unable to initialise chequebook (owner: %v): %v", lp.owner.Hex(), err) - } - } - - lp.chbook.AutoDeposit(lp.AutoDepositInterval, lp.AutoDepositThreshold, lp.AutoDepositBuffer) - log.Info(fmt.Sprintf("auto deposit ON for %v -> %v: interval = %v, threshold = %v, buffer = %v)", crypto.PubkeyToAddress(*(lp.publicKey)).Hex()[:8], lp.Contract.Hex()[:8], lp.AutoDepositInterval, lp.AutoDepositThreshold, lp.AutoDepositBuffer)) - - return nil -} - -/* -// Add (n) -// n > 0 called when promised/provided n units of service -// n < 0 called when used/requested n units of service -func (swap *Swap) Add(n int) error { - //defer swap.lock.Unlock() - //swap.lock.Lock() - swap.balance += n - if !swap.Sells && swap.balance > 0 { - log.Trace(fmt.Sprintf("<%v> remote peer cannot have debt (balance: %v)", swap.proto, swap.balance)) - swap.proto.Drop() - return fmt.Errorf("[SWAP] <%v> remote peer cannot have debt (balance: %v)", swap.proto, swap.balance) - } - if !swap.Buys && swap.balance < 0 { - log.Trace(fmt.Sprintf("<%v> we cannot have debt (balance: %v)", swap.proto, swap.balance)) - return fmt.Errorf("[SWAP] <%v> we cannot have debt (balance: %v)", swap.proto, swap.balance) - } - if swap.balance >= int(swap.local.DropAt) { - log.Trace(fmt.Sprintf("<%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", swap.proto, swap.balance, swap.local.DropAt)) - swap.proto.Drop() - return fmt.Errorf("[SWAP] <%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", swap.proto, swap.balance, swap.local.DropAt) - } else if swap.balance <= -int(swap.remote.PayAt) { - swap.send() - } - return nil -} - -// Balance accessor -func (swap *Swap) Balance() int { - //defer swap.lock.Unlock() - //swap.lock.Lock() - return swap.balance -} - -/* -// send (units) is called when payment is due -// In case of insolvency no promise is issued and sent, safe against fraud -// No return value: no error = payment is opportunistic = hang in till dropped -func (swap *Swap) send() { - if swap.local.BuyAt != nil && swap.balance < 0 { - amount := big.NewInt(int64(-swap.balance)) - amount.Mul(amount, swap.remote.SellAt) - promise, err := swap.Out.Issue(amount) - if err != nil { - log.Warn(fmt.Sprintf("<%v> cannot issue cheque (amount: %v, channel: %v): %v", swap.proto, amount, swap.Out, err)) - } else { - log.Warn(fmt.Sprintf("<%v> cheque issued (amount: %v, channel: %v)", swap.proto, amount, swap.Out)) - swap.proto.Pay(-swap.balance, promise) - swap.balance = 0 - } - } -} - -// Receive (units, promise) is called by the protocol when a payment msg is received -// returns error if promise is invalid. -func (swap *Swap) Receive(units int, promise Promise) error { - if units <= 0 { - return fmt.Errorf("invalid units: %v <= 0", units) - } - - price := new(big.Int).SetInt64(int64(units)) - price.Mul(price, swap.local.SellAt) - - amount, err := swap.In.Receive(promise) - - if err != nil { - err = fmt.Errorf("invalid promise: %v", err) - } else if price.Cmp(amount) != 0 { - // verify amount = units * unit sale price - return fmt.Errorf("invalid amount: %v = %v * %v (units sent in msg * agreed sale unit price) != %v (signed in cheque)", price, units, swap.local.SellAt, amount) - } - if err != nil { - log.Trace(fmt.Sprintf("<%v> invalid promise (amount: %v, channel: %v): %v", swap.proto, amount, swap.In, err)) - return err - } - - // credit remote peer with units - swap.Add(-units) - log.Trace(fmt.Sprintf("<%v> received promise (amount: %v, channel: %v): %v", swap.proto, amount, swap.In, promise)) - - return nil -} -*/ diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 403716125f..e6b3f6da98 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -57,6 +57,9 @@ var testSpec = &protocols.Spec{ }, } +//dummy implementation of a MsgReadWriter +//this allows for quick and easy unit tests without +//having to build up the complete protocol type dummyRW struct{} func (d *dummyRW) WriteMsg(msg p2p.Msg) error { @@ -72,21 +75,27 @@ func (d *dummyRW) ReadMsg() (p2p.Msg, error) { }, nil } +//define a couple of messages for tests type testExceedsPayAtMsg struct{} type testExceedsDropAtMsg struct{} type testCheapMsg struct{} -func (tmsg *testExceedsPayAtMsg) GetMsgPrice() *big.Int { +//this message is just one unit more expensive than the payment threshold +func (tmsg *testExceedsPayAtMsg) Price() *big.Int { diff := &big.Int{} - return diff.Sub(payAt, big.NewInt(1)) + diff = diff.Abs(payAt) + return diff.Add(diff, big.NewInt(1)) } -func (tmsg *testExceedsDropAtMsg) GetMsgPrice() *big.Int { +//this message is just one unit more expensive than the disconnect threshold +func (tmsg *testExceedsDropAtMsg) Price() *big.Int { diff := &big.Int{} - return diff.Sub(dropAt, big.NewInt(1)) + diff = diff.Abs(dropAt) + return diff.Add(diff, big.NewInt(1)) } -func (tmsg *testCheapMsg) GetMsgPrice() *big.Int { +//a message with an arbitrary cost +func (tmsg *testCheapMsg) Price() *big.Int { return big.NewInt(100) } @@ -97,6 +106,7 @@ func init() { log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) } +//check that the disconnect threshold is below the payment threshold func TestLimits(t *testing.T) { if dropAt.Cmp(payAt) > -1 { t.Fatal(fmt.Sprintf("dropAt limit is not lower than payAt limit, dropAt: %s, payAt: %s", dropAt.String(), payAt.String())) @@ -104,17 +114,29 @@ func TestLimits(t *testing.T) { } //unit test for exceeds pay limit +//when the payment threshold is reached, a cheque will be issued +//this test checks that a cheque is present if a message is sent +//which exceeds the payment threshold +//(note: the details of cheque handling will need to be fleshed out +//in future iterations, current implementation is very primitive) func TestExceedsPayAt(t *testing.T) { + //create a test swap account swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) + //create a dummy peer testPeer := newDummyPeer() sp := NewSwapPeer(testPeer, swap) sp.handlerFunc = dummyMsgHandler + //send the message ctx := context.Background() - sp.Send(ctx, &testExceedsPayAtMsg{}) + err := sp.Send(ctx, &testExceedsPayAtMsg{}) + if err != nil { + t.Fatal("Unecpected error on sending message", "err", err) + } + //check that a cheque is present cheques := sp.swapAccount.chequeManager.openDebitCheques[sp.ID()] if cheques == nil { t.Fatal("Expected cheques for this peer to be present, but are nil") @@ -132,6 +154,8 @@ func TestExceedsPayAt(t *testing.T) { } //unit test for exceeds drop limit +//tests that a message is being sent which crosses the drop limit +//in that case, we should receive a InsufficientFunds error func TestExceedsDropAt(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) @@ -147,15 +171,19 @@ func TestExceedsDropAt(t *testing.T) { } } +//send a message with cost, +//then check that the balance has the expected amount func TestSendCheapMessage(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) testPeer := newDummyPeer() sp := NewSwapPeer(testPeer, swap) + //set an arbitrary test balance value testBalance := big.NewInt(1234567890) sp.balance = testBalance + //send the message msg := &testCheapMsg{} ctx := context.Background() err := sp.Send(ctx, msg) @@ -163,19 +191,30 @@ func TestSendCheapMessage(t *testing.T) { t.Fatal("Unexpected error sending message") } - if sp.balance.Cmp(testBalance.Sub(testBalance, msg.GetMsgPrice())) != 0 { + //check the new balance + if sp.balance.Cmp(testBalance.Sub(testBalance, msg.Price())) != 0 { t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", - testBalance.Sub(testBalance, msg.GetMsgPrice()).String(), sp.balance.String())) + testBalance.Sub(testBalance, msg.Price()).String(), sp.balance.String())) } } +//try restoring a balance from state store +//this is simulated by creating a node, +//assigning it an arbitrary balance, +//send a message (triggers to save to store), +//then create a different SwapPeer instance with same peerID, +//which will try to load a balance from the stateStore func TestRestoreBalanceFromStateStore(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) + //create the dummy p2p protocol Peer testPeer := newDummyPeer() + //create the "source" swap peer sp := NewSwapPeer(testPeer, swap) + //create a reference an arbitrary balance testBalance := big.NewInt(1234567890) + //assign the same value to the peer sp.balance = big.NewInt(1234567890) //send a message, should trigger saving to stateStore @@ -187,16 +226,22 @@ func TestRestoreBalanceFromStateStore(t *testing.T) { t.Fatal("Unexpected error sending message") } + //create a new peer with same underlying protocols.Peer + //this will try to load the balance from the stateStore, + //as it is the same discover.NodeID sp2 := NewSwapPeer(testPeer, swap) + //compare the balances expectedBalance := &big.Int{} - expectedBalance.Sub(testBalance, msg.GetMsgPrice()) + expectedBalance.Sub(testBalance, msg.Price()) if sp2.balance.Cmp(expectedBalance) != 0 { t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", expectedBalance.String(), sp2.balance.String())) } } +//create a test swap account +//creates a stateStore for persistence and a Swap account func createTestSwap(t *testing.T) (*Swap, string) { dir, err := ioutil.TempDir("", "swap_test_store") if err != nil { @@ -206,22 +251,19 @@ func createTestSwap(t *testing.T) (*Swap, string) { if err2 != nil { t.Fatal(err2) } - swap, err3 := NewSwap(NewDefaultSwapParams().Params, stateStore) + swap, err3 := New(stateStore) if err3 != nil { t.Fatal(err3) } return swap, dir } -func runProtocol(peer *protocols.Peer, swap *Swap) { - sp := NewSwapPeer(peer, swap) - sp.Peer.Run(dummyMsgHandler) -} - +//dummy message handler (needed or we will have a panic in the accounting) func dummyMsgHandler(ctx context.Context, msg interface{}) error { return nil } +//tests some basic things over RPC func TestSwapRPC(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) @@ -236,7 +278,7 @@ func TestSwapRPC(t *testing.T) { // wrapper function for servicenode to start the service swapsvc := func(ctx *node.ServiceContext) (node.Service, error) { return &API{ - SwapProtocol: instance, + Protocol: instance, }, nil } @@ -314,11 +356,13 @@ func TestSwapRPC(t *testing.T) { } } +//creates a dummy protocols.Peer with dummy MsgReadWriter func newDummyPeer() *protocols.Peer { id := adapters.RandomNodeConfig().ID return protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), &dummyRW{}, testSpec) } +//creates a p2p.Service node stub func newServiceNode(port int, httpport int, wsport int, modules ...string) (*node.Node, error) { cfg := &node.DefaultConfig cfg.P2P.ListenAddr = fmt.Sprintf(":%d", port) From 55f45d25236f357af80c92839caa96e8ecaec58d Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 30 Aug 2018 11:49:08 -0500 Subject: [PATCH 11/30] swarm/swap: new swap accounting with SwapEnabled flag --- swarm/swap/swap.go | 23 ++++++++++------- swarm/swap_test.go | 64 +++++++++++++++++++++++++++++++++++++--------- swarm/swarm.go | 17 +++++------- 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index c31c528906..5b7c29906b 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -110,7 +110,8 @@ func (sp *SwapPeer) handle(ctx context.Context, msg interface{}) error { var price *big.Int //the message is one which needs accounting... - if _, ok := msg.(PricedMsg); ok { + //only account if swapAccount != nil (== swap is disabled) + if _, ok := msg.(PricedMsg); ok && sp.swapAccount != nil { //..so first check if there are enough funds for the operation available //(for crediting, this means if we are not essentially "overdrafting", or crossing the threshold) price, err = sp.checkAvailableFunds(ctx, msg, CreditEntry) @@ -144,7 +145,8 @@ func (sp *SwapPeer) Send(ctx context.Context, msg interface{}) error { var price *big.Int //the message is one which needs accounting... - if _, ok := msg.(PricedMsg); ok { + //only account if swapAccount != nil (== swap is disabled) + if _, ok := msg.(PricedMsg); ok && sp.swapAccount != nil { //..so first check if there are enough funds for the operation available price, err = sp.checkAvailableFunds(ctx, msg, DebitEntry) //if not (or some other error occured), return error @@ -263,18 +265,21 @@ func (sp *SwapPeer) issueCheque(ctx context.Context) error { //Create a new swap accounted peer func NewSwapPeer(peer *protocols.Peer, swap *Swap) *SwapPeer { - balance := big.NewInt(0) - //check if there is one already in the stateStore and load it - swap.stateStore.Get(peer.String()[:24]+"-swap", &balance) sp := &SwapPeer{ Peer: peer, swapAccount: swap, - balance: balance, storeID: peer.String()[:24] + "-swap", } - swap.lock.Lock() - defer swap.lock.Unlock() - swap.peers[peer.ID()] = sp + //swap is not enabled + if swap != nil { + //check if there is one already in the stateStore and load it + balance := &big.Int{} + swap.stateStore.Get(peer.String()[:24]+"-swap", &balance) + sp.balance = balance + swap.lock.Lock() + defer swap.lock.Unlock() + swap.peers[peer.ID()] = sp + } return sp } diff --git a/swarm/swap_test.go b/swarm/swap_test.go index dfca84197f..69fb702780 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -22,9 +22,16 @@ import ( "github.com/ethereum/go-ethereum/swarm/storage" ) +//In TestSwapNetworkSymmetricFileUpload we set up a network with arbitrary number of nodes +//(16), and each of the nodes uploads a file of same size +//Afterwards we check that every node's balance WITH ANOTHER PEER +//has the same value but opposite sign func TestSwapNetworkSymmetricFileUpload(t *testing.T) { + //default hardcoded network size nodeCount := 16 + //setup the simulation + //use a complete node setup via `NewSwam` sim := simulation.New(map[string]simulation.ServiceFunc{ "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { config := api.NewConfig() @@ -49,6 +56,9 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { config.Init(privkey) + //set Swap to be enabled for this test + config.SwapEnabled = true + swarm, err := NewSwarm(config, nil) if err != nil { return nil, cleanup, err @@ -67,16 +77,19 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { var nodeStatusM sync.Map var totalFoundCount uint64 + //connect all nodes in a chain _, err := sim.AddNodesAndConnectChain(nodeCount) if err != nil { t.Fatal(err) } + //run the simulation result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { nodeIDs := sim.UpNodeIDs() shuffle(len(nodeIDs), func(i, j int) { nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] }) + //upload a file for every node for _, id := range nodeIDs { key, data, err := uploadFile(sim.Service("swarm", id).(*Swarm)) if err != nil { @@ -90,6 +103,7 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { }) } + //wait for kademlia to be healthy if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { return err } @@ -103,8 +117,11 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { } }) + //every node has a map to all nodes it had interactions + //each entry in the map is a map of the other node with all the balances balancesMap := make(map[discover.NodeID]map[discover.NodeID]*big.Int) + //iterate all nodes for _, node := range sim.NodeIDs() { item, ok := sim.NodeItem(node, bucketKeySwarm) if !ok { @@ -113,12 +130,17 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { } swarm := item.(*Swarm) + //submap for each node is a map of all nodes with the balance for that node subBalances := make(map[discover.NodeID]*big.Int) + //iterate all nodes again... + //get all balances with other peers for every node for _, n := range sim.NodeIDs() { if node == n { continue } + + //get the peer's balance with this node balance := swarm.swap.GetPeerBalance(n) if balance != nil { subBalances[n] = balance @@ -127,9 +149,11 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) } } + //update the map for this node balancesMap[node] = subBalances } + //print all the balances if requested if *printStats { for k, v := range balancesMap { fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) @@ -139,12 +163,22 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { } } + //now iterate the whole map + //and check that every node k has the same + //balance with a peer as that peer with the node, + //but in inverted signs + + //iterate the map for k, mapForK := range balancesMap { + //iterate the submap for n, balanceKwithN := range mapForK { + //iterate the main map again for subK, mapForSubK := range balancesMap { + //if the node and the peer are the same... if n == subK { log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) + //...check that they have the same balance in Abs terms and that it is not 0 if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") } @@ -159,6 +193,8 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { log.Debug("test terminated") } +//TestSwapNetworkAsymmetricFileUpload is a swap test too, +//but this time the number and size of files are random func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { nodeCount := 16 @@ -185,6 +221,8 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { } config.Init(privkey) + //enable swap + config.SwapEnabled = true swarm, err := NewSwarm(config, nil) if err != nil { @@ -209,6 +247,9 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { t.Fatal(err) } + //this is actually quite a big maxFileSize, which results + //in the test running for nearly 2 minutes + //maybe for the test, we could reduce it const maxFileSize = 1024 * 1024 * 4 //1024 bytes * 1024 * 4 = 4MB const minfileSize = 1024 @@ -289,22 +330,21 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { } /* - TODO: Should balances in this case also be symmetric? - - for k, mapForK := range balancesMap { - for n, balanceKwithN := range mapForK { - for subK, mapForSubK := range balancesMap { - if n == subK { - log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) - log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) - if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { - log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") - } + Assuming that in this case, balances should be symmetric too I + */ + for k, mapForK := range balancesMap { + for n, balanceKwithN := range mapForK { + for subK, mapForSubK := range balancesMap { + if n == subK { + log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) + log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) + if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { + log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") } } } } - */ + } if result.Error != nil { t.Fatal(result.Error) diff --git a/swarm/swarm.go b/swarm/swarm.go index acf29777dd..29ca6bfc89 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -178,6 +178,12 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e if err := nodeID.UnmarshalText([]byte(config.NodeID)); err != nil { return nil, err } + if config.SwapEnabled { + self.swap, err = swap.New(stateStore) + if err != nil { + return nil, err + } + } self.streamer = stream.NewRegistry(nodeID, delivery, self.netStore, stateStore, &stream.RegistryOptions{ SkipCheck: config.DeliverySkipCheck, DoSync: config.SyncEnabled, @@ -344,7 +350,7 @@ func (self *Swarm) Start(srv *p2p.Server) error { newaddr := self.bzz.UpdateLocalAddr([]byte(srv.Self().String())) log.Info("Updated bzz local addr", "oaddr", fmt.Sprintf("%x", newaddr.OAddr), "uaddr", fmt.Sprintf("%s", newaddr.UAddr)) // set chequebook - if self.config.SwapEnabled { + if self.config.SwapEnabled && self.config.SwapAPI != "" { ctx := context.Background() // The initial setup has no deadline. err := self.SetChequebook(ctx) if err != nil { @@ -364,15 +370,6 @@ func (self *Swarm) Start(srv *p2p.Server) error { } log.Info("Swarm network started", "bzzaddr", fmt.Sprintf("%x", self.bzz.Hive.BaseAddr())) - /* - err = self.swap.Start(srv) - if err != nil { - log.Error("swap failed", "err", err) - return err - } - log.Debug("Swap accounting initialized") - */ - if self.ps != nil { self.ps.Start(srv) } From 958a3fb4479acb40f2d25c1e8c0ddfbc61ee716c Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Tue, 18 Sep 2018 08:54:39 -0500 Subject: [PATCH 12/30] swarm/swap: addressing PR comments --- swarm/network/stream/delivery.go | 2 +- swarm/network_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/swarm/network/stream/delivery.go b/swarm/network/stream/delivery.go index 4b494f1e06..3c5e7fed9e 100644 --- a/swarm/network/stream/delivery.go +++ b/swarm/network/stream/delivery.go @@ -139,7 +139,7 @@ type RetrieveRequestMsg struct { } //TODO: what is the correct price -func (rrm *RetrieveRequestMsg) GetMsgPrice() *big.Int { +func (rrm *RetrieveRequestMsg) Price() *big.Int { return big.NewInt(int64(4096)) } diff --git a/swarm/network_test.go b/swarm/network_test.go index 4771676856..81eb8005fa 100644 --- a/swarm/network_test.go +++ b/swarm/network_test.go @@ -44,7 +44,6 @@ var ( longrunning = flag.Bool("longrunning", false, "do run long-running tests") waitKademlia = flag.Bool("waitkademlia", false, "wait for healthy kademlia before checking files availability") printStats = flag.Bool("printstats", false, "print accounting stats to STDOUT") - bucketKeySwap = simulation.BucketKey("swap") bucketKeySwarm = simulation.BucketKey("swarm") ) From 006ff30c789809982050e5d0ab968b47b00f9b01 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Fri, 21 Sep 2018 19:45:04 -0500 Subject: [PATCH 13/30] swarm: alternative swap implementation via event subscribe --- p2p/peer.go | 4 ++ swarm/swap/swap.go | 134 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/p2p/peer.go b/p2p/peer.go index af019d07a8..8245de20a1 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -125,6 +125,10 @@ func NewPeer(id enode.ID, name string, caps []Cap) *Peer { return peer } +func (p *Peer) Events() *event.Feed { + return p.events +} + // ID returns the node's public key. func (p *Peer) ID() enode.ID { return p.rw.node.ID() diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 5b7c29906b..b21c24e9b9 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -24,9 +24,11 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/swarm/log" + "github.com/ethereum/go-ethereum/swarm/network/stream" "github.com/ethereum/go-ethereum/swarm/state" ) @@ -54,6 +56,7 @@ const ( // A node maintains an individual balance with every peer // Only messages which have a price will be accounted for type Swap struct { + priceOracle PriceOracle chequeManager *ChequeManager stateStore state.Store lock sync.RWMutex @@ -71,19 +74,141 @@ type SwapPeer struct { storeID string } +type PriceOracle interface { + IsAccountedMsg(event *p2p.PeerEvent) bool + GetPriceForMsg(event *p2p.PeerEvent) *PriceTag +} + //This defines if a price will be debited or credited to an account type EntryDirection bool const ( - DebitEntry EntryDirection = true - CreditEntry EntryDirection = false + ChargeSender EntryDirection = true + ChargeReceiver EntryDirection = false ) +type PriceTag struct { + Price *big.Int + SizeBased bool + Direction EntryDirection +} + +type DefaultPriceOracle struct { + priceMatrix map[string]map[int]*PriceTag +} + +func (dpo *DefaultPriceOracle) LoadPriceMatrix() { + priceMatrix := dpo.loadDefaultPriceMatrix() +} + +func (dpo *DefaultPriceOracle) loadDefaultPriceMatrix() map[string]map[int]*PriceTag { + priceMatrix := make(map[string]map[int]*big.Int) + + streamProtocol := stream.Spec.Name + + priceMatrix[streamProtocol] = make(map[int]*PriceTag) + + retrieveRequestMsgIndex := dpo.getIndexForMsgType(stream.RetrieveRequestMsg, stream.Spec.Messages) + deliveryMsgIndex := dpo.getIndexForMsgType(stream.ChunkDeliveryMsg, stream.Spec.Messages) + + priceMatrix[streamProtocol][retrieveRequestMsgIndex] = &PriceTag{ + Price: big.NewInt(10), + SizeBased: false, + Direction: ChargeSender, + } + priceMatrix[streamProtocol][deliveryMsgIndex] = &PriceTag{ + Price: big.NewInt(100), + SizeBased: true, + Direction: ChargeReceiver, + } + + return priceMatrix + +} + +func (dpo *DefaultPriceOracle) getIndexForMsgType(iface interface{}, types []interface{}) { + for i, v := range types { + if iface == v { + return i + } + } + return -1 +} + +func (dpo *DefaultPriceOracle) IsAccountedMsg(event *p2p.PeerEvent) bool { + if protoPriceMap, ok := dpo.priceMatrix[event.Protocol]; !ok { + return false + } + if msgCode, ok := protoPriceMatrix[event.MsgCode]; !ok { + return false + } + return true +} + +func (dpo *DefaultPriceOracle) GetPriceForMsg(event *p2p.PeerEvent) (*big.Int, EntryDirection) { + if dpo.IsAccountedMsg(event) { + priceTag := dpo.priceMatrix[event.Protocol][event.MsgCode] + price := priceTag.Price + if priceTag.SizeBased { + price = price * event.MsgSize + } + return price, priceTag.Direction + } + return nil, false +} + //A message which needs accounting needs to implement this interface type PricedMsg interface { Price() *big.Int } +func (s *Swap) RegisterForEvents(srv *p2p.Server) { + go func() { + events := make(chan *p2p.PeerEvent) + sub := server.SubscribeEvents(events) + defer sub.Unsubscribe() + + for { + select { + case event := <-events: + go handleMsgEvent(event) + case <-sub.Err(): + return + } + } + }() +} + +func (s *Swap) handleMsgEvent(event *p2p.PeerEvent) { + if !s.priceOracle.IsAccountedMsg(event) { + return + } + s.AccountMsgForPeer(event) +} + +func (s *Swap) AccountMsgForPeer(event *p2p.PeerEvent) { + price, direction := s.priceOracle.GetPriceForMsg(event) + if price == nil { + //TODO what to do in this case? Should not happen + panic("This should be accounted for but somehow it failed") + } + + if direction == ChargeSender { + if event.Type == p2p.PeerEventTypeMsgSend { + s.chargeSender(event, price) + } else if event.Type == p2p.PeerEventTypeMsgRecv { + s.chargeReceiver(event, price) + } + } else { + if event.Type == p2p.PeerEventTypeMsgRecv { + s.chargeReceiver(event, price) + } else if event.Type == p2p.PeerEventTypeMsgSend { + s.chargeSender(event, price) + } + } + +} + //Handler for received messages func (sp *SwapPeer) RunAccountedProtocol(protocolHandler func(ctx context.Context, msg interface{}) error) error { //the `peer.Run` function is a loop, so in order to pre-/post-process a message with accounting, @@ -284,13 +409,16 @@ func NewSwapPeer(peer *protocols.Peer, swap *Swap) *SwapPeer { } // New - swap constructor -func New(stateStore state.Store) (swap *Swap, err error) { +func New(stateStore state.Store, srv *p2p.Server) (swap *Swap, err error) { swap = &Swap{ chequeManager: NewChequeManager(stateStore), stateStore: stateStore, peers: make(map[discover.NodeID]*SwapPeer), + priceOracle: &DefaultPriceOracle{}, } + swap.RegisterForEvents(srv) + return } From f5d736d107ccd1a2a9a4bf1b4b946f2906a48757 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Mon, 24 Sep 2018 09:21:49 -0500 Subject: [PATCH 14/30] swarm/network/stream: Syncing flag for ChunkDeliveryMsg --- swarm/network/stream/delivery.go | 10 ++++++---- swarm/network/stream/messages.go | 3 ++- swarm/network/stream/peer.go | 7 ++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/swarm/network/stream/delivery.go b/swarm/network/stream/delivery.go index 3c5e7fed9e..7a7eabcc4a 100644 --- a/swarm/network/stream/delivery.go +++ b/swarm/network/stream/delivery.go @@ -180,7 +180,8 @@ func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req * return } if req.SkipCheck { - err = sp.Deliver(ctx, chunk, s.priority) + syncing := false + err = sp.Deliver(ctx, chunk, s.priority, syncing) if err != nil { log.Warn("ERROR in handleRetrieveRequestMsg", "err", err) } @@ -197,9 +198,10 @@ func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req * } type ChunkDeliveryMsg struct { - Addr storage.Address - SData []byte // the stored chunk Data (incl size) - peer *Peer // set in handleChunkDeliveryMsg + Addr storage.Address + SData []byte // the stored chunk Data (incl size) + peer *Peer // set in handleChunkDeliveryMsg + Syncing bool // if true, this is a delivery for syncing (no SWAP accounting needed) } // TODO: Fix context SNAFU diff --git a/swarm/network/stream/messages.go b/swarm/network/stream/messages.go index 74c785d587..636c995a70 100644 --- a/swarm/network/stream/messages.go +++ b/swarm/network/stream/messages.go @@ -347,7 +347,8 @@ func (p *Peer) handleWantedHashesMsg(ctx context.Context, req *WantedHashesMsg) return fmt.Errorf("handleWantedHashesMsg get data %x: %v", hash, err) } chunk := storage.NewChunk(hash, data) - if err := p.Deliver(ctx, chunk, s.priority); err != nil { + syncing := true + if err := p.Deliver(ctx, chunk, s.priority, syncing); err != nil { return err } } diff --git a/swarm/network/stream/peer.go b/swarm/network/stream/peer.go index f7ad57b032..d0a32b0b5f 100644 --- a/swarm/network/stream/peer.go +++ b/swarm/network/stream/peer.go @@ -130,7 +130,7 @@ func NewPeer(peer *protocols.Peer, streamer *Registry) *Peer { } // Deliver sends a storeRequestMsg protocol message to the peer -func (p *Peer) Deliver(ctx context.Context, chunk storage.Chunk, priority uint8) error { +func (p *Peer) Deliver(ctx context.Context, chunk storage.Chunk, priority uint8, syncing bool) error { var sp opentracing.Span ctx, sp = spancontext.StartSpan( ctx, @@ -138,8 +138,9 @@ func (p *Peer) Deliver(ctx context.Context, chunk storage.Chunk, priority uint8) defer sp.Finish() msg := &ChunkDeliveryMsg{ - Addr: chunk.Address(), - SData: chunk.Data(), + Addr: chunk.Address(), + SData: chunk.Data(), + Syncing: syncing, } return p.SendPriority(ctx, msg, priority) } From 24cc67cc0758470680f9c6547a57f6708bf32ab1 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Mon, 24 Sep 2018 10:14:50 -0500 Subject: [PATCH 15/30] swarm/swap: bug fixes --- swarm/swap/swap.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index b21c24e9b9..f0a3fd960d 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -139,7 +139,7 @@ func (dpo *DefaultPriceOracle) IsAccountedMsg(event *p2p.PeerEvent) bool { if protoPriceMap, ok := dpo.priceMatrix[event.Protocol]; !ok { return false } - if msgCode, ok := protoPriceMatrix[event.MsgCode]; !ok { + if msgCode, ok := protoPriceMap[event.MsgCode]; !ok { return false } return true @@ -190,7 +190,7 @@ func (s *Swap) AccountMsgForPeer(event *p2p.PeerEvent) { price, direction := s.priceOracle.GetPriceForMsg(event) if price == nil { //TODO what to do in this case? Should not happen - panic("This should be accounted for but somehow it failed") + log.Crit("Price is nil; this should have been accounted for but somehow it failed") } if direction == ChargeSender { From fc0bf4030aef89a89eda0c559b1d5c93187d5bab Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Mon, 24 Sep 2018 11:28:42 -0500 Subject: [PATCH 16/30] swarm/network/stream: Different chunk delivery msg types for syncing and retrieval --- swarm/network/stream/delivery.go | 10 ++++++++++ swarm/network/stream/peer.go | 22 ++++++++++++++++++---- swarm/network/stream/stream.go | 12 +++++++++--- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/swarm/network/stream/delivery.go b/swarm/network/stream/delivery.go index 7a7eabcc4a..0b36026e4f 100644 --- a/swarm/network/stream/delivery.go +++ b/swarm/network/stream/delivery.go @@ -197,6 +197,7 @@ func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req * return nil } +//Chunk delivery always uses the same message type.... type ChunkDeliveryMsg struct { Addr storage.Address SData []byte // the stored chunk Data (incl size) @@ -204,6 +205,15 @@ type ChunkDeliveryMsg struct { Syncing bool // if true, this is a delivery for syncing (no SWAP accounting needed) } +//...but swap accounting needs to disambiguate if it is a delivery for syncing or for retrieval +//as it decides based on message type if it needs to account for this message or not + +//defines a chunk delivery for retrieval (with accounting) +type ChunkDeliveryMsgRetrieval ChunkDeliveryMsg + +//defines a chunk delivery for syncing (without accounting) +type ChunkDeliveryMsgSyncing ChunkDeliveryMsg + // TODO: Fix context SNAFU func (d *Delivery) handleChunkDeliveryMsg(ctx context.Context, sp *Peer, req *ChunkDeliveryMsg) error { var osp opentracing.Span diff --git a/swarm/network/stream/peer.go b/swarm/network/stream/peer.go index d0a32b0b5f..1b383aa88f 100644 --- a/swarm/network/stream/peer.go +++ b/swarm/network/stream/peer.go @@ -130,6 +130,7 @@ func NewPeer(peer *protocols.Peer, streamer *Registry) *Peer { } // Deliver sends a storeRequestMsg protocol message to the peer +// Depending on the `syncing` parameter we send different message types func (p *Peer) Deliver(ctx context.Context, chunk storage.Chunk, priority uint8, syncing bool) error { var sp opentracing.Span ctx, sp = spancontext.StartSpan( @@ -137,10 +138,23 @@ func (p *Peer) Deliver(ctx context.Context, chunk storage.Chunk, priority uint8, "send.chunk.delivery") defer sp.Finish() - msg := &ChunkDeliveryMsg{ - Addr: chunk.Address(), - SData: chunk.Data(), - Syncing: syncing, + var msg interface{} + + //we send different types of messages if delivery is for syncing or retrievals, + //even if handling and content of the message are the same, + //because swap accounting decides which messages need accounting based on the message type + if syncing { + msg = &ChunkDeliveryMsgSyncing{ + Addr: chunk.Address(), + SData: chunk.Data(), + Syncing: syncing, + } + } else { + msg = &ChunkDeliveryMsgRetrieval{ + Addr: chunk.Address(), + SData: chunk.Data(), + Syncing: syncing, + } } return p.SendPriority(ctx, msg, priority) } diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index 004c42d860..58609f5c48 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -482,8 +482,13 @@ func (p *Peer) HandleMsg(ctx context.Context, msg interface{}) error { case *WantedHashesMsg: return p.handleWantedHashesMsg(ctx, msg) - case *ChunkDeliveryMsg: - return p.streamer.delivery.handleChunkDeliveryMsg(ctx, p, msg) + case *ChunkDeliveryMsgRetrieval: + //handling chunk delivery is the same for retrieval and syncing, so let's cast the msg + return p.streamer.delivery.handleChunkDeliveryMsg(ctx, p, ((*ChunkDeliveryMsg)(msg))) + + case *ChunkDeliveryMsgSyncing: + //handling chunk delivery is the same for retrieval and syncing, so let's cast the msg + return p.streamer.delivery.handleChunkDeliveryMsg(ctx, p, ((*ChunkDeliveryMsg)(msg))) case *RetrieveRequestMsg: return p.streamer.delivery.handleRetrieveRequestMsg(ctx, p, msg) @@ -683,10 +688,11 @@ var Spec = &protocols.Spec{ TakeoverProofMsg{}, SubscribeMsg{}, RetrieveRequestMsg{}, - ChunkDeliveryMsg{}, + ChunkDeliveryMsgRetrieval{}, SubscribeErrorMsg{}, RequestSubscriptionMsg{}, QuitMsg{}, + ChunkDeliveryMsgSyncing{}, }, } From 12d825e379a6528c122c07c7605d1cab02260179 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Tue, 25 Sep 2018 22:15:09 -0500 Subject: [PATCH 17/30] swarm: swap implementation with event subscription to p2p server --- swarm/network/stream/delivery.go | 7 - swarm/network/stream/peer.go | 6 +- swarm/network/stream/stream.go | 6 +- swarm/swap/api.go | 12 +- swarm/swap/protocol.go | 80 +++---- swarm/swap/swap.go | 397 +++++++++++++------------------ swarm/swap/swap_test.go | 392 +++++++++++++++++++++++------- swarm/swap_test.go | 2 + swarm/swarm.go | 12 + 9 files changed, 526 insertions(+), 388 deletions(-) diff --git a/swarm/network/stream/delivery.go b/swarm/network/stream/delivery.go index 0b36026e4f..483f02112b 100644 --- a/swarm/network/stream/delivery.go +++ b/swarm/network/stream/delivery.go @@ -19,8 +19,6 @@ package stream import ( "context" "errors" - "math/big" - "time" "fmt" @@ -138,11 +136,6 @@ type RetrieveRequestMsg struct { HopCount uint8 } -//TODO: what is the correct price -func (rrm *RetrieveRequestMsg) Price() *big.Int { - return big.NewInt(int64(4096)) -} - func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req *RetrieveRequestMsg) error { log.Trace("received request", "peer", sp.ID(), "hash", req.Addr) handleRetrieveRequestMsgCount.Inc(1) diff --git a/swarm/network/stream/peer.go b/swarm/network/stream/peer.go index 1b383aa88f..45c851634c 100644 --- a/swarm/network/stream/peer.go +++ b/swarm/network/stream/peer.go @@ -31,7 +31,6 @@ import ( "github.com/ethereum/go-ethereum/swarm/spancontext" "github.com/ethereum/go-ethereum/swarm/state" "github.com/ethereum/go-ethereum/swarm/storage" - "github.com/ethereum/go-ethereum/swarm/swap" opentracing "github.com/opentracing/opentracing-go" ) @@ -54,8 +53,7 @@ var ErrMaxPeerServers = errors.New("max peer servers") // Peer is the Peer extension for the streaming protocol type Peer struct { - //*protocols.Peer - *swap.SwapPeer + *protocols.Peer streamer *Registry pq *pq.PriorityQueue serverMu sync.RWMutex @@ -77,7 +75,7 @@ type WrappedPriorityMsg struct { // NewPeer is the constructor for Peer func NewPeer(peer *protocols.Peer, streamer *Registry) *Peer { p := &Peer{ - SwapPeer: swap.NewSwapPeer(peer, streamer.swap), + Peer: peer, pq: pq.New(int(PriorityQueue), PriorityQueueCap), streamer: streamer, servers: make(map[Stream]*server), diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index 58609f5c48..fe975f40e4 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -34,8 +34,6 @@ import ( "github.com/ethereum/go-ethereum/swarm/pot" "github.com/ethereum/go-ethereum/swarm/state" "github.com/ethereum/go-ethereum/swarm/storage" - "github.com/ethereum/go-ethereum/swarm/swap" - opentracing "github.com/opentracing/opentracing-go" ) const ( @@ -104,8 +102,6 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy RegisterSwarmSyncerServer(streamer, syncChunkStore) RegisterSwarmSyncerClient(streamer, syncChunkStore) - streamer.swap = swap - if options.DoSync { // latestIntC function ensures that // - receiving from the in chan is not blocked by processing inside the for loop @@ -385,7 +381,7 @@ func (r *Registry) Run(p *network.BzzPeer) error { } } - return sp.RunAccountedProtocol(sp.HandleMsg) + return sp.Run(sp.HandleMsg) } // updateSyncing subscribes to SYNC streams by iterating over the diff --git a/swarm/swap/api.go b/swarm/swap/api.go index c224d43f2f..b6d2a8a149 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -30,7 +30,7 @@ var ( //This is the API definition to access swarm swap accounting data via RPC type API struct { - *Protocol + swap *Swap } //TODO: define metrics @@ -39,13 +39,13 @@ type SwapMetrics struct { } //Create a new API instance -func NewAPI(swap *Protocol) *API { - return &API{Protocol: swap} +func NewAPI(swap *Swap) *API { + return &API{swap: swap} } //Get the balance for this node with a specific peer func (swapapi *API) BalanceWithPeer(ctx context.Context, peer discover.NodeID) (balance *big.Int, err error) { - balance = swapapi.swap.peers[peer].balance + balance = swapapi.swap.peers[peer] if balance == nil { err = ErrNoSuchPeerAccounting } @@ -58,8 +58,8 @@ func (swapapi *API) BalanceWithPeer(ctx context.Context, peer discover.NodeID) ( //It assumes that if a disfavorable balance is represented as a negative value func (swapapi *API) Balance(ctx context.Context) (balance *big.Int, err error) { balance = big.NewInt(0) - for _, peer := range swapapi.swap.peers { - balance.Add(balance, peer.balance) + for _, peerBalance := range swapapi.swap.peers { + balance.Add(balance, peerBalance) } return } diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 0ee3fc575d..601079a1a6 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -21,7 +21,6 @@ import ( "fmt" "sync" - "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/protocols" @@ -42,29 +41,25 @@ const ( type Protocol struct { peersMu sync.RWMutex peers map[discover.NodeID]*Peer - swap *Swap } //This is the peer representing a participant in this protocol type Peer struct { *protocols.Peer - swapProtocol *Protocol } //Create a new protocol instance -func NewSwapProtocol(swapAccount *Swap) *Protocol { +func NewProtocol() *Protocol { proto := &Protocol{ peers: make(map[discover.NodeID]*Peer), - swap: swapAccount, } return proto } // NewPeer is the constructor for the protocol Peer -func NewPeer(peer *protocols.Peer, swap *Protocol) *Peer { +func NewPeer(peer *protocols.Peer) *Peer { p := &Peer{ - Peer: peer, - swapProtocol: swap, + Peer: peer, } return p } @@ -79,17 +74,19 @@ type IssueChequeMsg struct { //redeem a cheque, which means it kicks off the process to cash it in. //In this case, this message is sent to peer A type RedeemChequeMsg struct { + Cheque *Cheque } ///////////////////////////////////////////////////////////////////// // SECTION: node.Service interface ///////////////////////////////////////////////////////////////////// -func (p *Protocol) Start(srv *p2p.Server) error { +func (s *Swap) Start(srv *p2p.Server) error { + s.registerForEvents(srv) log.Debug("Started swap") return nil } -func (p *Protocol) Stop() error { +func (s *Swap) Stop() error { log.Info("swap shutting down") return nil } @@ -104,23 +101,23 @@ var swapSpec = &protocols.Spec{ }, } -func (p *Protocol) Protocols() []p2p.Protocol { +func (s *Swap) Protocols() []p2p.Protocol { return []p2p.Protocol{ { Name: swapSpec.Name, Version: swapSpec.Version, Length: swapSpec.Length(), - Run: p.Run, + Run: s.run, }, } } -func (p *Protocol) APIs() []rpc.API { +func (s *Swap) APIs() []rpc.API { apis := []rpc.API{ { Namespace: "swap", Version: "1.0", - Service: NewAPI(p), + Service: NewAPI(s), Public: true, }, } @@ -130,55 +127,52 @@ func (p *Protocol) APIs() []rpc.API { ///////////////////////////////////////////////////////////////////// // SECTION: p2p.protocol interface ///////////////////////////////////////////////////////////////////// -func (swap *Protocol) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { +func (s *Swap) run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { p := protocols.NewPeer(peer, rw, swapSpec) - sp := NewPeer(p, swap) - swap.setPeer(sp) - defer swap.deletePeer(sp) - defer swap.Close() + sp := NewPeer(p) + s.protocol.setPeer(sp) + defer s.protocol.deletePeer(sp) + defer s.protocol.Close() return sp.Run(sp.handleSwapMsg) } -func (swap *Protocol) NodeInfo() interface{} { +func (s *Swap) NodeInfo() interface{} { return nil } -func (swap *Protocol) PeerInfo(id discover.NodeID) interface{} { +func (s *Swap) PeerInfo(id discover.NodeID) interface{} { return nil } //------------------------------------------------------------------------------------------ -func (swap *Protocol) Close() { +func (proto *Protocol) Close() { } -func (swap *Protocol) getPeer(peerId discover.NodeID) *Peer { - swap.peersMu.RLock() - defer swap.peersMu.RUnlock() +func (proto *Protocol) getPeer(peerId discover.NodeID) *Peer { + proto.peersMu.RLock() + defer proto.peersMu.RUnlock() - return swap.peers[peerId] + return proto.peers[peerId] } -func (swap *Protocol) setPeer(peer *Peer) { - swap.peersMu.Lock() - defer swap.peersMu.Unlock() +func (proto *Protocol) setPeer(peer *Peer) { + proto.peersMu.Lock() + defer proto.peersMu.Unlock() - swap.peers[peer.ID()] = peer - metrics.GetOrRegisterGauge("swap.peers", nil).Update(int64(len(swap.peers))) + proto.peers[peer.ID()] = peer } -func (swap *Protocol) deletePeer(peer *Peer) { - swap.peersMu.Lock() - defer swap.peersMu.Unlock() +func (proto *Protocol) deletePeer(peer *Peer) { + proto.peersMu.Lock() + defer proto.peersMu.Unlock() - delete(swap.peers, peer.ID()) - metrics.GetOrRegisterGauge("swap.peers", nil).Update(int64(len(swap.peers))) - swap.peersMu.Unlock() + delete(proto.peers, peer.ID()) } -func (swap *Protocol) peersCount() (c int) { - swap.peersMu.Lock() - c = len(swap.peers) - swap.peersMu.Unlock() +func (proto *Protocol) peersCount() (c int) { + proto.peersMu.Lock() + c = len(proto.peers) + proto.peersMu.Unlock() return } @@ -198,13 +192,13 @@ func (p *Peer) handleSwapMsg(ctx context.Context, msg interface{}) error { } //A IssueChequeMsg has been received -func (sp *Peer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { +func (p *Peer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { log.Debug("SwapProtocolPeer: handleIssueChequeMsg") return err } //A RedeemChequeMsg has been received -func (sp *Peer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { +func (p *Peer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { log.Debug("SwapProtocolPeer: handleRedeemChequeMsg") return err } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index f0a3fd960d..05bda5f400 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -26,7 +26,6 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/discover" - "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/network/stream" "github.com/ethereum/go-ethereum/swarm/state" @@ -42,8 +41,8 @@ var ( payAt = big.NewInt(-4096 * 10000) // threshold that triggers payment {request} (bytes) dropAt = big.NewInt(-4096 * 12000) // threshold that triggers disconnect (bytes) - ErrInsufficientFunds = errors.New("Insufficient funds") ErrNotAccountedMsg = errors.New("Message does not need accounting") + ErrInsufficientFunds = errors.New("Insufficient funds") ) const ( @@ -60,63 +59,80 @@ type Swap struct { chequeManager *ChequeManager stateStore state.Store lock sync.RWMutex - peers map[discover.NodeID]*SwapPeer -} - -//Protocols which want to send and handle priced messages will need to use -//this peer instead of protocols.Peer, which is embedded -type SwapPeer struct { - *protocols.Peer - lock sync.RWMutex - swapAccount *Swap - handlerFunc func(context.Context, interface{}) error - balance *big.Int - storeID string + peers map[discover.NodeID]*big.Int + p2pServer *p2p.Server + protocol *Protocol } +//PriceOracle is responsible to maintain the price matrix for accounted messages, +//to get its price and to evaluate if a message is accountable or not type PriceOracle interface { IsAccountedMsg(event *p2p.PeerEvent) bool - GetPriceForMsg(event *p2p.PeerEvent) *PriceTag + GetPriceForMsg(event *p2p.PeerEvent) (*big.Int, EntryDirection) } //This defines if a price will be debited or credited to an account type EntryDirection bool +//For some messages the sender pays (e.g. RetrieveRequestMsg), +//for others the receiver (ChunkDeliveryMsg) const ( ChargeSender EntryDirection = true ChargeReceiver EntryDirection = false ) +//An accountable message needs some meta information attached to it +//in order to evaluate the correct price type PriceTag struct { Price *big.Int SizeBased bool Direction EntryDirection } +//The default price oracle type DefaultPriceOracle struct { - priceMatrix map[string]map[int]*PriceTag + priceMatrix map[string]map[uint64]*PriceTag } +//Load a default price matrix func (dpo *DefaultPriceOracle) LoadPriceMatrix() { - priceMatrix := dpo.loadDefaultPriceMatrix() + dpo.priceMatrix = dpo.loadDefaultPriceMatrix() } -func (dpo *DefaultPriceOracle) loadDefaultPriceMatrix() map[string]map[int]*PriceTag { - priceMatrix := make(map[string]map[int]*big.Int) +//Set up a default price matrix +//This statically builds the price matrix according to previous knowledge +//of which messages need accounting +//The matrix can be queried by +// * the protocol string, returns a sub-map +// * the sub-map by the message code, which returns the PriceTag +func (dpo *DefaultPriceOracle) loadDefaultPriceMatrix() map[string]map[uint64]*PriceTag { + //setup matrix + priceMatrix := make(map[string]map[uint64]*PriceTag) + //define accounted messages from the stream protocol streamProtocol := stream.Spec.Name + priceMatrix[streamProtocol] = make(map[uint64]*PriceTag) - priceMatrix[streamProtocol] = make(map[int]*PriceTag) + //get indexes of accounted messages in order to populate the map + retrieveRequestMsgIndex, ok := stream.Spec.GetCode(stream.RetrieveRequestMsg{}) + if !ok { + log.Crit("setting up price matrix failed; expected message code not found in Spec!") + return nil + } + deliveryMsgIndex, ok := stream.Spec.GetCode(stream.RetrieveRequestMsg{}) + if !ok { + log.Crit("setting up price matrix failed; expected message code not found in Spec!") + return nil + } - retrieveRequestMsgIndex := dpo.getIndexForMsgType(stream.RetrieveRequestMsg, stream.Spec.Messages) - deliveryMsgIndex := dpo.getIndexForMsgType(stream.ChunkDeliveryMsg, stream.Spec.Messages) - - priceMatrix[streamProtocol][retrieveRequestMsgIndex] = &PriceTag{ + //assign the price tag to the message + priceMatrix[streamProtocol][uint64(retrieveRequestMsgIndex)] = &PriceTag{ Price: big.NewInt(10), SizeBased: false, Direction: ChargeSender, } - priceMatrix[streamProtocol][deliveryMsgIndex] = &PriceTag{ + + priceMatrix[streamProtocol][uint64(deliveryMsgIndex)] = &PriceTag{ Price: big.NewInt(100), SizeBased: true, Direction: ChargeReceiver, @@ -126,59 +142,57 @@ func (dpo *DefaultPriceOracle) loadDefaultPriceMatrix() map[string]map[int]*Pric } -func (dpo *DefaultPriceOracle) getIndexForMsgType(iface interface{}, types []interface{}) { - for i, v := range types { - if iface == v { - return i - } - } - return -1 -} - +//Check if a message needs accounting func (dpo *DefaultPriceOracle) IsAccountedMsg(event *p2p.PeerEvent) bool { - if protoPriceMap, ok := dpo.priceMatrix[event.Protocol]; !ok { + var protoPriceMap map[uint64]*PriceTag + var ok bool + if protoPriceMap, ok = dpo.priceMatrix[event.Protocol]; !ok { return false } - if msgCode, ok := protoPriceMap[event.MsgCode]; !ok { + if _, ok = protoPriceMap[*event.MsgCode]; !ok { return false } return true } +//Get the actual price of a message +//If the message is size-based, it returns the calculated price func (dpo *DefaultPriceOracle) GetPriceForMsg(event *p2p.PeerEvent) (*big.Int, EntryDirection) { if dpo.IsAccountedMsg(event) { - priceTag := dpo.priceMatrix[event.Protocol][event.MsgCode] - price := priceTag.Price + priceTag := dpo.priceMatrix[event.Protocol][*event.MsgCode] + price := &big.Int{} + price.Set(priceTag.Price) if priceTag.SizeBased { - price = price * event.MsgSize + price.Mul(priceTag.Price, big.NewInt(int64(*event.MsgSize))) } return price, priceTag.Direction } return nil, false } -//A message which needs accounting needs to implement this interface -type PricedMsg interface { - Price() *big.Int -} - -func (s *Swap) RegisterForEvents(srv *p2p.Server) { +//This swap implementation works by listening to message events on the p2p server. +//It then handles the event received, filtering for messages and evaluating +//if it needs accounting +func (s *Swap) registerForEvents(srv *p2p.Server) { go func() { events := make(chan *p2p.PeerEvent) - sub := server.SubscribeEvents(events) + sub := srv.SubscribeEvents(events) defer sub.Unsubscribe() for { select { case event := <-events: - go handleMsgEvent(event) - case <-sub.Err(): + go s.handleMsgEvent(event) + case err := <-sub.Err(): + log.Error(err.Error()) return } } }() } +//Handle the message. +//Determine if it needs accounting, and if yes, account for it func (s *Swap) handleMsgEvent(event *p2p.PeerEvent) { if !s.priceOracle.IsAccountedMsg(event) { return @@ -186,6 +200,9 @@ func (s *Swap) handleMsgEvent(event *p2p.PeerEvent) { s.AccountMsgForPeer(event) } +//Do the accounting +//Depending on the charging type of the message (set in the PriceTag), +//it will charge the sender or receiver func (s *Swap) AccountMsgForPeer(event *p2p.PeerEvent) { price, direction := s.priceOracle.GetPriceForMsg(event) if price == nil { @@ -194,231 +211,137 @@ func (s *Swap) AccountMsgForPeer(event *p2p.PeerEvent) { } if direction == ChargeSender { + //we are sending a ChargeSender message, thus debit us and credit remote if event.Type == p2p.PeerEventTypeMsgSend { - s.chargeSender(event, price) + s.chargeLocal(event, price) + //we are receiving a ChargeSender message, so credit us and debit remote } else if event.Type == p2p.PeerEventTypeMsgRecv { - s.chargeReceiver(event, price) + s.chargeRemote(event, price) } } else { + //we are receiving a ChargeReceiver message, thus debit us and credit remote if event.Type == p2p.PeerEventTypeMsgRecv { - s.chargeReceiver(event, price) + s.chargeLocal(event, price) + //we are sending a ChargeReceiver message, thus credit us and debit remote } else if event.Type == p2p.PeerEventTypeMsgSend { - s.chargeSender(event, price) + s.chargeRemote(event, price) } } - } -//Handler for received messages -func (sp *SwapPeer) RunAccountedProtocol(protocolHandler func(ctx context.Context, msg interface{}) error) error { - //the `peer.Run` function is a loop, so in order to pre-/post-process a message with accounting, - //we need to save the actual handler - sp.handlerFunc = protocolHandler +//Debit us and credit remote +func (s *Swap) chargeLocal(event *p2p.PeerEvent, amount *big.Int) { + s.lock.Lock() + defer s.lock.Unlock() + //local node is being credited (in its favor), so its balance increases + if s.peers[event.Peer] == nil { + s.peers[event.Peer] = &big.Int{} + s.stateStore.Get(event.Peer.String(), s.peers[event.Peer]) + } + peerBalance := s.peers[event.Peer] + peerBalance.Sub(peerBalance, amount) + //local node is being debited (in favor of remote peer), so its balance decreases + //TODO: save to store here? init store? + s.stateStore.Put(event.Peer.String(), peerBalance) + //(balance *Int) Cmp(payAt) + // -1 if balance < payAt + // 0 if balance == payAt + // +1 if balance > payAt + if peerBalance.Cmp(payAt) == -1 { + ctx := context.TODO() + err := s.issueCheque(ctx, event.Peer) + if err != nil { + //TODO: special error handling, as at this point the accounting has been done + //but the cheque could not be sent? + log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) + } + } + if peerBalance.Cmp(dropAt) == -1 { + peer := s.protocol.getPeer(event.Peer) + if peer != nil { + peer.Drop(ErrInsufficientFunds) + //this little hack allows for tests to verify that this error occurred + event.Error = ErrInsufficientFunds.Error() + } + } + log.Debug(fmt.Sprintf("balance for peer %s: %s", event.Peer.String(), peerBalance.String())) +} - //then run the handler loop function - return sp.Run(sp.handle) +//Credit us and debit remote +func (s *Swap) chargeRemote(event *p2p.PeerEvent, amount *big.Int) { + s.lock.Lock() + defer s.lock.Unlock() + //local node is being credited (in its favor), so its balance increases + if s.peers[event.Peer] == nil { + s.peers[event.Peer] = &big.Int{} + } + peerBalance := s.peers[event.Peer] + peerBalance.Add(peerBalance, amount) + //local node is being credited(in favor of local peer), so its balance increases + //TODO: save to store here? init store? + s.stateStore.Put(event.Peer.String(), peerBalance) + //(balance *Int) Cmp(payAt) + // -1 if balance < payAt + // 0 if balance == payAt + // +1 if balance > payAt + if peerBalance.Cmp(payAt) == -1 { + ctx := context.TODO() + err := s.issueCheque(ctx, event.Peer) + if err != nil { + //TODO: special error handling, as at this point the accounting has been done + //but the cheque could not be sent? + log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) + } + } + if peerBalance.Cmp(dropAt) == -1 { + peer := s.protocol.getPeer(event.Peer) + if peer != nil { + //this little hack allows for tests to verify that this error occurred + peer.Drop(ErrInsufficientFunds) + } + } + log.Debug(fmt.Sprintf("balance for peer %s: %s", event.Peer.String(), peerBalance.String())) } //get a peer's balance func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { + swap.lock.RLock() + defer swap.lock.RUnlock() if p, ok := swap.peers[peer]; ok { - return p.balance + return p } return nil } -//Handle a received message; this is the handler loop function. -//Check if it needs accounting, and if yes, apply accounting logic: -//Check for sufficient funds, perform operation, then account -func (sp *SwapPeer) handle(ctx context.Context, msg interface{}) error { - var err error - var price *big.Int - - //the message is one which needs accounting... - //only account if swapAccount != nil (== swap is disabled) - if _, ok := msg.(PricedMsg); ok && sp.swapAccount != nil { - //..so first check if there are enough funds for the operation available - //(for crediting, this means if we are not essentially "overdrafting", or crossing the threshold) - price, err = sp.checkAvailableFunds(ctx, msg, CreditEntry) - //if not (or some other error occured), return error - if err != nil { - //also, if the error is indeed insufficient funds, then disconnect the peer - if err == ErrInsufficientFunds { - log.Error("Insufficient funds, dropping peer") - sp.Drop(err) - } - return err - } - //at this point we know there are sufficient funds, so process the message - err = sp.handlerFunc(ctx, msg) - if err == nil { - //and if no errors occurred, finally book the entry - sp.AccountMsgForPeer(ctx, msg, price, CreditEntry) - } - } else { - //this message doesn't need accounting, so just process it - err = sp.handlerFunc(ctx, msg) - } - return err -} - -//Send a message -//Check if it needs accounting, and if yes, apply accounting logic: -//Check for sufficient funds, perform operation, then account -func (sp *SwapPeer) Send(ctx context.Context, msg interface{}) error { - var err error - var price *big.Int - - //the message is one which needs accounting... - //only account if swapAccount != nil (== swap is disabled) - if _, ok := msg.(PricedMsg); ok && sp.swapAccount != nil { - //..so first check if there are enough funds for the operation available - price, err = sp.checkAvailableFunds(ctx, msg, DebitEntry) - //if not (or some other error occured), return error - if err != nil { - //also, if the error is indeed insufficient funds, then disconnect the peer - if err == ErrInsufficientFunds { - log.Error("Insufficient funds, dropping peer") - sp.Drop(err) - } - return err - } - //at this point we know there are sufficient funds, so process the message - err = sp.Peer.Send(ctx, msg) - if err == nil { - //and if no errors occurred, finally book the entry - sp.AccountMsgForPeer(ctx, msg, price, DebitEntry) - } - } else { - //this message doesn't need accounting, so just process it - err = sp.Peer.Send(ctx, msg) - } - return err -} - -//check that the operation has enough funds available -func (sp *SwapPeer) checkAvailableFunds(ctx context.Context, msg interface{}, direction EntryDirection) (*big.Int, error) { - sp.lock.Lock() - defer sp.lock.Unlock() - - if accounted, ok := msg.(PricedMsg); ok { - price := accounted.Price() - //local node is being credited (in its favor), so check upper limit - if direction == CreditEntry { - //TODO: is there a check needed here? - //It should actually have been done on the client side, the debitor! - //creditor could theoretically go over payAt, but if well done, - //should have been checked on the client side so this shouldn't happen? - checkBalance := &big.Int{} - checkBalance.Add(sp.balance, price) - //(checkBalance *Int) CmpAbs(payAt) - // -1 if |checkBalance| < |payAt| - // 0 if |checkBalance| == |payAt| - // +1 if |checkBalance| > |payAt| - if checkBalance.CmpAbs(payAt) == 1 { - return nil, ErrInsufficientFunds - } - } else if direction == DebitEntry { - //NOTE: ErrInsufficientFunds should only be returned - //if the dropAt is exceeded, but should be ignored for payAt, - //as there is a "clemency" margin between triggering the check - //and actually disconnecting the peer - - //(checkBalance *Int) Cmp(dropAt) - // -1 if checkBalance < dropAt - // 0 if checkBalance == dropAt - // +1 if checkBalance > dropAt - checkBalance := &big.Int{} - checkBalance.Sub(sp.balance, price.Abs(price)) - if checkBalance.Cmp(dropAt) == -1 { - return nil, ErrInsufficientFunds - } - } - return price, nil - } - return nil, ErrNotAccountedMsg -} - -//The balance is accounted from the point of view of the local node -//Thus, we credit the balance and increase it when the amount is in favor of the local node -//We debit the balance and decrease it when the amount is in favor of the remote peer -func (sp *SwapPeer) AccountMsgForPeer(ctx context.Context, msg interface{}, price *big.Int, direction EntryDirection) { - if _, ok := msg.(PricedMsg); ok { - sp.lock.Lock() - defer sp.lock.Unlock() - //local node is being credited (in its favor), so its balance increases - if direction == CreditEntry { - //NOTE: do we need to check for sufficient funds again? - //operations are not atomic/transactional, so balance may have changed in the meanwhile! - sp.balance.Add(sp.balance, price) - //local node is being debited (in favor of remote peer), so its balance decreases - } else if direction == DebitEntry { - sp.balance.Sub(sp.balance, price) - } - //TODO: save to store here? init store? - sp.swapAccount.stateStore.Put(sp.storeID, sp.balance) - //(sp.balance *Int) Cmp(payAt) - // -1 if sp.balance < payAt - // 0 if sp.balance == payAt - // +1 if sp.balance > payAt - if sp.balance.Cmp(payAt) == -1 { - err := sp.issueCheque(ctx) - if err != nil { - //TODO: special error handling, as at this point the accounting has been done - //but the cheque could not be sent? - log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) - } - } - if sp.balance.Cmp(dropAt) == -1 { - sp.Drop(ErrInsufficientFunds) - } - log.Debug(fmt.Sprintf("balance for peer %s: %s", sp.ID(), sp.balance.String())) - } -} - //Issue a cheque for the remote peer. Happens if we are indebted with the peer //and crossed the payment threshold -func (sp *SwapPeer) issueCheque(ctx context.Context) error { +func (s *Swap) issueCheque(ctx context.Context, id discover.NodeID) error { amount := &big.Int{} - cheque := sp.swapAccount.chequeManager.CreateCheque(sp.ID(), amount.Abs(payAt)) + cheque := s.chequeManager.CreateCheque(id, amount.Abs(payAt)) msg := IssueChequeMsg{ Cheque: cheque, } //TODO: This should now be via the actual SwapProtocol - return sp.Send(ctx, msg) -} - -//Create a new swap accounted peer -func NewSwapPeer(peer *protocols.Peer, swap *Swap) *SwapPeer { - sp := &SwapPeer{ - Peer: peer, - swapAccount: swap, - storeID: peer.String()[:24] + "-swap", + p := s.protocol.getPeer(id) + if p == nil { + return fmt.Errorf("wanting to send to non-connected peer!") } - //swap is not enabled - if swap != nil { - //check if there is one already in the stateStore and load it - balance := &big.Int{} - swap.stateStore.Get(peer.String()[:24]+"-swap", &balance) - sp.balance = balance - swap.lock.Lock() - defer swap.lock.Unlock() - swap.peers[peer.ID()] = sp - } - return sp + return p.Send(ctx, msg) } // New - swap constructor -func New(stateStore state.Store, srv *p2p.Server) (swap *Swap, err error) { +func New(stateStore state.Store) (swap *Swap) { + + priceOracle := &DefaultPriceOracle{} swap = &Swap{ chequeManager: NewChequeManager(stateStore), stateStore: stateStore, - peers: make(map[discover.NodeID]*SwapPeer), - priceOracle: &DefaultPriceOracle{}, + peers: make(map[discover.NodeID]*big.Int), + priceOracle: priceOracle, + protocol: NewProtocol(), } - - swap.RegisterForEvents(srv) + priceOracle.LoadPriceMatrix() return } diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index e6b3f6da98..144cd13687 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 The go-ethereum Authors +// Copyreeeeight 2018 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 @@ -54,6 +54,8 @@ var testSpec = &protocols.Spec{ testExceedsPayAtMsg{}, testExceedsDropAtMsg{}, testCheapMsg{}, + testSizeBasedMsg{}, + testChargeRecvMsg{}, }, } @@ -79,6 +81,8 @@ func (d *dummyRW) ReadMsg() (p2p.Msg, error) { type testExceedsPayAtMsg struct{} type testExceedsDropAtMsg struct{} type testCheapMsg struct{} +type testSizeBasedMsg struct{} +type testChargeRecvMsg struct{} //this message is just one unit more expensive than the payment threshold func (tmsg *testExceedsPayAtMsg) Price() *big.Int { @@ -99,6 +103,16 @@ func (tmsg *testCheapMsg) Price() *big.Int { return big.NewInt(100) } +//a message which needs to be charged based on price +func (tmsg *testSizeBasedMsg) Price() *big.Int { + return big.NewInt(222) +} + +//a message which needs to be charged based on price +func (tmsg *testChargeRecvMsg) Price() *big.Int { + return big.NewInt(999) +} + func init() { flag.Parse() @@ -124,20 +138,32 @@ func TestExceedsPayAt(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - //create a dummy peer - testPeer := newDummyPeer() - sp := NewSwapPeer(testPeer, swap) - sp.handlerFunc = dummyMsgHandler + swap.priceOracle = setupPriceMatrix() - //send the message - ctx := context.Background() - err := sp.Send(ctx, &testExceedsPayAtMsg{}) - if err != nil { - t.Fatal("Unecpected error on sending message", "err", err) + node := createAndStartSvcNode(swap, t) + defer node.Stop() + defer os.RemoveAll(node.DataDir()) + + peerID := adapters.RandomNodeConfig().ID + + code, ok := testSpec.GetCode(&testExceedsPayAtMsg{}) + if !ok { + t.Fatal("test message not found in spec") } + event := &p2p.PeerEvent{ + Type: p2p.PeerEventTypeMsgSend, + Protocol: testSpec.Name, + Peer: peerID, + MsgCode: &code, + MsgSize: new(uint32), + Error: "", + } + + swap.handleMsgEvent(event) + //check that a cheque is present - cheques := sp.swapAccount.chequeManager.openDebitCheques[sp.ID()] + cheques := swap.chequeManager.openDebitCheques[peerID] if cheques == nil { t.Fatal("Expected cheques for this peer to be present, but are nil") } @@ -157,44 +183,83 @@ func TestExceedsPayAt(t *testing.T) { //tests that a message is being sent which crosses the drop limit //in that case, we should receive a InsufficientFunds error func TestExceedsDropAt(t *testing.T) { + //create a test swap account swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - testPeer := newDummyPeer() - sp := NewSwapPeer(testPeer, swap) - sp.handlerFunc = dummyMsgHandler + swap.priceOracle = setupPriceMatrix() - ctx := context.Background() - err := sp.Send(ctx, &testExceedsDropAtMsg{}) - if err != ErrInsufficientFunds { - t.Fatal("Expected test to fail with insufficient funds, but it didn't") + node := createAndStartSvcNode(swap, t) + defer node.Stop() + defer os.RemoveAll(node.DataDir()) + + //peerID := adapters.RandomNodeConfig().ID + peer := newDummyPeer() + swap.protocol.setPeer(peer.Peer) + + code, ok := testSpec.GetCode(&testExceedsDropAtMsg{}) + if !ok { + t.Fatal("test message not found in spec") + } + + event := &p2p.PeerEvent{ + Type: p2p.PeerEventTypeMsgSend, + Protocol: testSpec.Name, + Peer: peer.ID(), + MsgCode: &code, + MsgSize: new(uint32), + Error: "", + } + + swap.handleMsgEvent(event) + if event.Error != ErrInsufficientFunds.Error() { + t.Fatal("Excpected this test to fail with insufficient funds, but it did not") } } //send a message with cost, //then check that the balance has the expected amount func TestSendCheapMessage(t *testing.T) { + fmt.Println("send") swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - testPeer := newDummyPeer() - sp := NewSwapPeer(testPeer, swap) + swap.priceOracle = setupPriceMatrix() + + node := createAndStartSvcNode(swap, t) + defer node.Stop() + defer os.RemoveAll(node.DataDir()) + + //peerID := adapters.RandomNodeConfig().ID + peer := newDummyPeer() + swap.protocol.setPeer(peer.Peer) //set an arbitrary test balance value testBalance := big.NewInt(1234567890) - sp.balance = testBalance + swap.peers[peer.ID()] = testBalance - //send the message - msg := &testCheapMsg{} - ctx := context.Background() - err := sp.Send(ctx, msg) - if err != nil { - t.Fatal("Unexpected error sending message") + code, ok := testSpec.GetCode(&testCheapMsg{}) + if !ok { + t.Fatal("test message not found in spec") } + event := &p2p.PeerEvent{ + Type: p2p.PeerEventTypeMsgSend, + Protocol: testSpec.Name, + Peer: peer.ID(), + MsgCode: &code, + MsgSize: new(uint32), + Error: "", + } + + swap.handleMsgEvent(event) + + peerBalance := swap.peers[peer.ID()] + msg := &testCheapMsg{} + //check the new balance - if sp.balance.Cmp(testBalance.Sub(testBalance, msg.Price())) != 0 { + if peerBalance.Cmp(testBalance.Sub(testBalance, msg.Price())) != 0 { t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", - testBalance.Sub(testBalance, msg.Price()).String(), sp.balance.String())) + testBalance.Sub(testBalance, msg.Price()).String(), peerBalance.String())) } } @@ -208,35 +273,142 @@ func TestRestoreBalanceFromStateStore(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - //create the dummy p2p protocol Peer - testPeer := newDummyPeer() - //create the "source" swap peer - sp := NewSwapPeer(testPeer, swap) + swap.priceOracle = setupPriceMatrix() + + node := createAndStartSvcNode(swap, t) + defer node.Stop() + defer os.RemoveAll(node.DataDir()) + + //peerID := adapters.RandomNodeConfig().ID + peer := newDummyPeer() + swap.protocol.setPeer(peer.Peer) //create a reference an arbitrary balance testBalance := big.NewInt(1234567890) //assign the same value to the peer - sp.balance = big.NewInt(1234567890) + swap.peers[peer.ID()] = big.NewInt(1234567890) - //send a message, should trigger saving to stateStore - msg := &testCheapMsg{} - ctx := context.Background() - err := sp.Send(ctx, msg) - if err != nil { - log.Error(err.Error()) - t.Fatal("Unexpected error sending message") + code, ok := testSpec.GetCode(&testCheapMsg{}) + if !ok { + t.Fatal("test message not found in spec") } - //create a new peer with same underlying protocols.Peer - //this will try to load the balance from the stateStore, - //as it is the same discover.NodeID - sp2 := NewSwapPeer(testPeer, swap) + event := &p2p.PeerEvent{ + Type: p2p.PeerEventTypeMsgSend, + Protocol: testSpec.Name, + Peer: peer.ID(), + MsgCode: &code, + MsgSize: new(uint32), + Error: "", + } + + swap.handleMsgEvent(event) + + peerBalance := swap.peers[peer.ID()] + expectedBalance := &big.Int{} + swap.stateStore.Get(peer.ID().String(), expectedBalance) //compare the balances - expectedBalance := &big.Int{} - expectedBalance.Sub(testBalance, msg.Price()) - if sp2.balance.Cmp(expectedBalance) != 0 { + expectedBalance.Sub(testBalance, (&testCheapMsg{}).Price()) + if peerBalance.Cmp(expectedBalance) != 0 { t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", - expectedBalance.String(), sp2.balance.String())) + expectedBalance.String(), peerBalance.String())) + } +} + +//send a message with cost, +//then check that the balance has the expected amount +//the message is charged size based +func TestSendSizeBasedMsg(t *testing.T) { + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + swap.priceOracle = setupPriceMatrix() + + node := createAndStartSvcNode(swap, t) + defer node.Stop() + defer os.RemoveAll(node.DataDir()) + + peer := newDummyPeer() + swap.protocol.setPeer(peer.Peer) + //set an arbitrary test balance value + testBalance := big.NewInt(1234567890) + swap.peers[peer.ID()] = testBalance + + code, ok := testSpec.GetCode(&testSizeBasedMsg{}) + if !ok { + t.Fatal("test message not found in spec") + } + + size := new(uint32) + *size = 500 + + event := &p2p.PeerEvent{ + Type: p2p.PeerEventTypeMsgSend, + Protocol: testSpec.Name, + Peer: peer.ID(), + MsgCode: &code, + MsgSize: size, + Error: "", + } + + swap.handleMsgEvent(event) + + peerBalance := swap.peers[peer.ID()] + msg := &testSizeBasedMsg{} + expectedBalance := &big.Int{} + expectedBalance.Mul(msg.Price(), big.NewInt(int64(*size))) + + //check the new balance + if peerBalance.Cmp(testBalance.Sub(testBalance, expectedBalance)) != 0 { + t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", + expectedBalance.String(), peerBalance.String())) + } +} + +//charge as being the receiver of a receiver-pays message +//as this test for simplicity only sends messages, +//it means that the balance is changed in positive, +//instead of negative as with all other tests +func TestChargeAsReceiver(t *testing.T) { + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + swap.priceOracle = setupPriceMatrix() + + node := createAndStartSvcNode(swap, t) + defer node.Stop() + defer os.RemoveAll(node.DataDir()) + + //peerID := adapters.RandomNodeConfig().ID + peer := newDummyPeer() + swap.protocol.setPeer(peer.Peer) + //set an arbitrary test balance value + testBalance := big.NewInt(1234567890) + swap.peers[peer.ID()] = testBalance + + code, ok := testSpec.GetCode(&testChargeRecvMsg{}) + if !ok { + t.Fatal("test message not found in spec") + } + + event := &p2p.PeerEvent{ + Type: p2p.PeerEventTypeMsgSend, + Protocol: testSpec.Name, + Peer: peer.ID(), + MsgCode: &code, + MsgSize: new(uint32), + Error: "", + } + + swap.handleMsgEvent(event) + + peerBalance := swap.peers[peer.ID()] + msg := &testChargeRecvMsg{} + + //check the new balance + if peerBalance.Cmp(testBalance.Add(testBalance, msg.Price())) != 0 { + t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", + testBalance.Add(testBalance, msg.Price()).String(), peerBalance.String())) } } @@ -251,61 +423,97 @@ func createTestSwap(t *testing.T) (*Swap, string) { if err2 != nil { t.Fatal(err2) } - swap, err3 := New(stateStore) - if err3 != nil { - t.Fatal(err3) - } + swap := New(stateStore) return swap, dir } +func setupPriceMatrix() PriceOracle { + + priceOracle := &DefaultPriceOracle{} + priceOracle.priceMatrix = make(map[string]map[uint64]*PriceTag) + priceOracle.priceMatrix[testSpec.Name] = make(map[uint64]*PriceTag) + protoMatrix := priceOracle.priceMatrix[testSpec.Name] + protoMatrix[0] = &PriceTag{ + Direction: ChargeSender, + Price: (&testExceedsPayAtMsg{}).Price(), + } + + protoMatrix[1] = &PriceTag{ + Direction: ChargeSender, + Price: (&testExceedsDropAtMsg{}).Price(), + } + + protoMatrix[2] = &PriceTag{ + Direction: ChargeSender, + Price: (&testCheapMsg{}).Price(), + } + + protoMatrix[3] = &PriceTag{ + Direction: ChargeSender, + Price: (&testSizeBasedMsg{}).Price(), + SizeBased: true, + } + + protoMatrix[4] = &PriceTag{ + Direction: ChargeReceiver, + Price: (&testChargeRecvMsg{}).Price(), + } + + return priceOracle +} + //dummy message handler (needed or we will have a panic in the accounting) func dummyMsgHandler(ctx context.Context, msg interface{}) error { return nil } -//tests some basic things over RPC -func TestSwapRPC(t *testing.T) { - swap, testDir := createTestSwap(t) - defer os.RemoveAll(testDir) - - // create the two nodes - stack_one, err := newServiceNode(p2pPort, 0, 0) +func createAndStartSvcNode(swap *Swap, t *testing.T) *node.Node { + stack, err := newServiceNode(p2pPort, 0, 0) if err != nil { t.Fatal("Create servicenode #1 fail", "err", err) } - instance := NewSwapProtocol(swap) - // wrapper function for servicenode to start the service swapsvc := func(ctx *node.ServiceContext) (node.Service, error) { - return &API{ - Protocol: instance, - }, nil + return swap, nil } - // register adds the service to the services the servicenode starts when started - err = stack_one.Register(swapsvc) + err = stack.Register(swapsvc) if err != nil { t.Fatal("Register service in servicenode #1 fail", "err", err) } + // start the nodes - err = stack_one.Start() + err = stack.Start() if err != nil { t.Fatal("servicenode #1 start failed", "err", err) } + return stack +} + +//tests some basic things over RPC +func TestSwapRPC(t *testing.T) { + + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + stack := createAndStartSvcNode(swap, t) + defer stack.Stop() + defer os.RemoveAll(stack.DataDir()) + // connect to the servicenode RPCs - rpcclient_one, err := rpc.Dial(filepath.Join(stack_one.DataDir(), ipcpath)) + rpcclient, err := rpc.Dial(filepath.Join(stack.DataDir(), ipcpath)) if err != nil { - t.Fatal("connect to servicenode #1 IPC fail", "err", err) + t.Fatal("connect to servicenode IPC fail", "err", err) } - defer os.RemoveAll(stack_one.DataDir()) + defer os.RemoveAll(stack.DataDir()) var balance *big.Int - err = rpcclient_one.Call(&balance, "swap_balance") + err = rpcclient.Call(&balance, "swap_balance") if err != nil { - t.Fatal("servicenode #1 RPC failed", "err", err) + t.Fatal("servicenode RPC failed", "err", err) } - log.Debug("servicenode #1 balance", "balance-1", balance) + log.Debug("servicenode balance", "balance", balance) if balance.Cmp(big.NewInt(0)) != 0 { t.Fatal("Expected balance to be 0 but it is not") @@ -321,32 +529,30 @@ func TestSwapRPC(t *testing.T) { fakeBalance1 := big.NewInt(fake1) fakeBalance2 := big.NewInt(fake2) - swap.peers[id1] = NewSwapPeer(dummyPeer1, swap) - swap.peers[id2] = NewSwapPeer(dummyPeer2, swap) - swap.peers[id1].balance = fakeBalance1 - swap.peers[id2].balance = fakeBalance2 + swap.peers[id1] = fakeBalance1 + swap.peers[id2] = fakeBalance2 - err = rpcclient_one.Call(&balance, "swap_balanceWithPeer", id1) + err = rpcclient.Call(&balance, "swap_balanceWithPeer", id1) if err != nil { - t.Fatal("servicenode #1 RPC failed", "err", err) + t.Fatal("servicenode RPC failed", "err", err) } log.Debug("balance1", "balance-1", balance) if balance.Cmp(fakeBalance1) != 0 { t.Fatal(fmt.Sprintf("Expected balance %s to be equal to fake balance %s, but it is not", balance.String(), fakeBalance1.String())) } - err = rpcclient_one.Call(&balance, "swap_balanceWithPeer", id2) + err = rpcclient.Call(&balance, "swap_balanceWithPeer", id2) if err != nil { - t.Fatal("servicenode #1 RPC failed", "err", err) + t.Fatal("servicenode RPC failed", "err", err) } log.Debug("balance2", "balance-2", balance) if balance.Cmp(fakeBalance2) != 0 { t.Fatal(fmt.Sprintf("Expected balance %s to be equal to fake balance %s, but it is not", balance.String(), fakeBalance2.String())) } - err = rpcclient_one.Call(&balance, "swap_balance") + err = rpcclient.Call(&balance, "swap_balance") if err != nil { - t.Fatal("servicenode #1 RPC failed", "err", err) + t.Fatal("servicenode RPC failed", "err", err) } log.Debug("balance", "balance", balance) @@ -356,10 +562,24 @@ func TestSwapRPC(t *testing.T) { } } +type dummyPeer struct { + *Peer + testFunc func(error) +} + +func (dp *dummyPeer) Drop(err error) { + fmt.Println("DD") + dp.testFunc(err) +} + //creates a dummy protocols.Peer with dummy MsgReadWriter -func newDummyPeer() *protocols.Peer { +func newDummyPeer() *dummyPeer { id := adapters.RandomNodeConfig().ID - return protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), &dummyRW{}, testSpec) + protoPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), &dummyRW{}, testSpec) + dummy := &dummyPeer{ + Peer: NewPeer(protoPeer), + } + return dummy } //creates a p2p.Service node stub diff --git a/swarm/swap_test.go b/swarm/swap_test.go index 69fb702780..ba8d120210 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -35,6 +35,7 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { sim := simulation.New(map[string]simulation.ServiceFunc{ "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { config := api.NewConfig() + config.Port = strconv.Itoa(8500 + rand.Intn(9999)) dir, err := ioutil.TempDir("", "swap-network-test-node") if err != nil { @@ -63,6 +64,7 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { if err != nil { return nil, cleanup, err } + bucket.Store(bucketKeySwarm, swarm) log.Info("new swarm", "bzzKey", config.BzzKey, "baseAddr", fmt.Sprintf("%x", swarm.bzz.BaseAddr())) return swarm, cleanup, nil diff --git a/swarm/swarm.go b/swarm/swarm.go index 29ca6bfc89..3a6c481fc0 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -215,6 +215,10 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e self.bzz = network.NewBzz(bzzconfig, to, stateStore, stream.Spec, self.streamer.Run) + if config.SwapEnabled { + self.swap = swap.New(stateStore) + } + // Pss = postal service over swarm (devp2p over bzz) self.ps, err = pss.NewPss(to, config.Pss) if err != nil { @@ -370,6 +374,10 @@ func (self *Swarm) Start(srv *p2p.Server) error { } log.Info("Swarm network started", "bzzaddr", fmt.Sprintf("%x", self.bzz.Hive.BaseAddr())) + if self.swap != nil { + self.swap.Start(srv) + } + if self.ps != nil { self.ps.Start(srv) } @@ -448,6 +456,10 @@ func (self *Swarm) Protocols() (protos []p2p.Protocol) { if self.ps != nil { protos = append(protos, self.ps.Protocols()...) } + + if self.swap != nil { + protos = append(protos, self.swap.Protocols()...) + } return } From 3d234cdbbbb6b37591d1059f8327d3fa49d9f91e Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Tue, 2 Oct 2018 16:34:57 -0500 Subject: [PATCH 18/30] swarm/swap: rebased on latest master --- swarm/swap/api.go | 4 +-- swarm/swap/chequemanager.go | 18 ++++++------- swarm/swap/protocol.go | 10 +++---- swarm/swap/swap.go | 53 +++++++++++++------------------------ swarm/swap/swap_test.go | 2 -- swarm/swap_test.go | 26 ++++++++++++++---- swarm/swarm.go | 6 ----- 7 files changed, 55 insertions(+), 64 deletions(-) diff --git a/swarm/swap/api.go b/swarm/swap/api.go index b6d2a8a149..15ace353dc 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -21,7 +21,7 @@ import ( "errors" "math/big" - "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/enode" ) var ( @@ -44,7 +44,7 @@ func NewAPI(swap *Swap) *API { } //Get the balance for this node with a specific peer -func (swapapi *API) BalanceWithPeer(ctx context.Context, peer discover.NodeID) (balance *big.Int, err error) { +func (swapapi *API) BalanceWithPeer(ctx context.Context, peer enode.ID) (balance *big.Int, err error) { balance = swapapi.swap.peers[peer] if balance == nil { err = ErrNoSuchPeerAccounting diff --git a/swarm/swap/chequemanager.go b/swarm/swap/chequemanager.go index a9e4494b0a..1fb2029d72 100644 --- a/swarm/swap/chequemanager.go +++ b/swarm/swap/chequemanager.go @@ -19,15 +19,15 @@ package swap import ( "math/big" - "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/swarm/state" ) type ChequeManager struct { stateStore state.Store - serialPerNode map[discover.NodeID]uint64 - openDebitCheques map[discover.NodeID][]*Cheque - openCreditCheques map[discover.NodeID][]*Cheque + serialPerNode map[enode.ID]uint64 + openDebitCheques map[enode.ID][]*Cheque + openCreditCheques map[enode.ID][]*Cheque } type Cheque struct { @@ -35,20 +35,20 @@ type Cheque struct { timeout uint64 amount *big.Int sumCumulated *big.Int - beneficiary discover.NodeID //this should probably be common.Address? + beneficiary enode.ID //this should probably be common.Address? } func NewChequeManager(stateStore state.Store) *ChequeManager { return &ChequeManager{ stateStore: stateStore, //TODO: restore from state store - serialPerNode: make(map[discover.NodeID]uint64), - openDebitCheques: make(map[discover.NodeID][]*Cheque), - openCreditCheques: make(map[discover.NodeID][]*Cheque), + serialPerNode: make(map[enode.ID]uint64), + openDebitCheques: make(map[enode.ID][]*Cheque), + openCreditCheques: make(map[enode.ID][]*Cheque), } } -func (mgr *ChequeManager) CreateCheque(beneficiary discover.NodeID, amount *big.Int) *Cheque { +func (mgr *ChequeManager) CreateCheque(beneficiary enode.ID, amount *big.Int) *Cheque { mgr.serialPerNode[beneficiary]++ cheque := &Cheque{ serial: mgr.serialPerNode[beneficiary], diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 601079a1a6..452160cd3a 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -22,7 +22,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/swarm/log" @@ -40,7 +40,7 @@ const ( //For a better understanding, please read the Swap network paper "Generalized Swap swear and swindle games" type Protocol struct { peersMu sync.RWMutex - peers map[discover.NodeID]*Peer + peers map[enode.ID]*Peer } //This is the peer representing a participant in this protocol @@ -51,7 +51,7 @@ type Peer struct { //Create a new protocol instance func NewProtocol() *Protocol { proto := &Protocol{ - peers: make(map[discover.NodeID]*Peer), + peers: make(map[enode.ID]*Peer), } return proto } @@ -140,7 +140,7 @@ func (s *Swap) NodeInfo() interface{} { return nil } -func (s *Swap) PeerInfo(id discover.NodeID) interface{} { +func (s *Swap) PeerInfo(id enode.ID) interface{} { return nil } @@ -148,7 +148,7 @@ func (s *Swap) PeerInfo(id discover.NodeID) interface{} { func (proto *Protocol) Close() { } -func (proto *Protocol) getPeer(peerId discover.NodeID) *Peer { +func (proto *Protocol) getPeer(peerId enode.ID) *Peer { proto.peersMu.RLock() defer proto.peersMu.RUnlock() diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 05bda5f400..e346ce1706 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -22,10 +22,9 @@ import ( "fmt" "math/big" "sync" - "time" "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/network/stream" "github.com/ethereum/go-ethereum/swarm/state" @@ -45,23 +44,17 @@ var ( ErrInsufficientFunds = errors.New("Insufficient funds") ) -const ( - chequebookDeployRetries = 5 - chequebookDeployDelay = 1 * time.Second // delay between retries -) - // SwAP Swarm Accounting Protocol // a peer to peer micropayment system // A node maintains an individual balance with every peer // Only messages which have a price will be accounted for type Swap struct { - priceOracle PriceOracle - chequeManager *ChequeManager - stateStore state.Store - lock sync.RWMutex - peers map[discover.NodeID]*big.Int - p2pServer *p2p.Server - protocol *Protocol + priceOracle PriceOracle //the price oracle facilitates message pricing to the swap instance + chequeManager *ChequeManager //cheque manager keeps track of issued cheques + stateStore state.Store //stateStore is needed in order to keep balances across sessions + lock sync.RWMutex //lock the balances + peers map[enode.ID]*big.Int //map of balances for each peer + protocol *Protocol //reference to the cheque exchange protocol } //PriceOracle is responsible to maintain the price matrix for accounted messages, @@ -197,35 +190,25 @@ func (s *Swap) handleMsgEvent(event *p2p.PeerEvent) { if !s.priceOracle.IsAccountedMsg(event) { return } - s.AccountMsgForPeer(event) + s.accountMsgForPeer(event) } //Do the accounting //Depending on the charging type of the message (set in the PriceTag), //it will charge the sender or receiver -func (s *Swap) AccountMsgForPeer(event *p2p.PeerEvent) { +func (s *Swap) accountMsgForPeer(event *p2p.PeerEvent) { price, direction := s.priceOracle.GetPriceForMsg(event) if price == nil { //TODO what to do in this case? Should not happen - log.Crit("Price is nil; this should have been accounted for but somehow it failed") + panic("Price is nil; this should have been accounted for but somehow it failed") } - if direction == ChargeSender { - //we are sending a ChargeSender message, thus debit us and credit remote - if event.Type == p2p.PeerEventTypeMsgSend { - s.chargeLocal(event, price) - //we are receiving a ChargeSender message, so credit us and debit remote - } else if event.Type == p2p.PeerEventTypeMsgRecv { - s.chargeRemote(event, price) - } + if chargeLocal := (direction == ChargeSender) == (event.Type == p2p.PeerEventTypeMsgSend); chargeLocal { + //debit local node and credit remote + s.chargeLocal(event, price) } else { - //we are receiving a ChargeReceiver message, thus debit us and credit remote - if event.Type == p2p.PeerEventTypeMsgRecv { - s.chargeLocal(event, price) - //we are sending a ChargeReceiver message, thus credit us and debit remote - } else if event.Type == p2p.PeerEventTypeMsgSend { - s.chargeRemote(event, price) - } + //credit local node and debit remote + s.chargeRemote(event, price) } } @@ -304,7 +287,7 @@ func (s *Swap) chargeRemote(event *p2p.PeerEvent, amount *big.Int) { } //get a peer's balance -func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { +func (swap *Swap) GetPeerBalance(peer enode.ID) *big.Int { swap.lock.RLock() defer swap.lock.RUnlock() if p, ok := swap.peers[peer]; ok { @@ -315,7 +298,7 @@ func (swap *Swap) GetPeerBalance(peer discover.NodeID) *big.Int { //Issue a cheque for the remote peer. Happens if we are indebted with the peer //and crossed the payment threshold -func (s *Swap) issueCheque(ctx context.Context, id discover.NodeID) error { +func (s *Swap) issueCheque(ctx context.Context, id enode.ID) error { amount := &big.Int{} cheque := s.chequeManager.CreateCheque(id, amount.Abs(payAt)) msg := IssueChequeMsg{ @@ -337,7 +320,7 @@ func New(stateStore state.Store) (swap *Swap) { swap = &Swap{ chequeManager: NewChequeManager(stateStore), stateStore: stateStore, - peers: make(map[discover.NodeID]*big.Int), + peers: make(map[enode.ID]*big.Int), priceOracle: priceOracle, protocol: NewProtocol(), } diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 144cd13687..c731745342 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -24,7 +24,6 @@ import ( "math/big" "os" "path/filepath" - "sync" "testing" "time" @@ -42,7 +41,6 @@ var ( p2pPort = 30100 ipcpath = ".swarm.ipc" datadirPrefix = ".data_" - stackW = &sync.WaitGroup{} loglevel = flag.Int("loglevel", 2, "verbosity of logs") ) diff --git a/swarm/swap_test.go b/swarm/swap_test.go index ba8d120210..fb0c5e4a35 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -1,3 +1,19 @@ +// Copyright 2018 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 swarm import ( @@ -14,7 +30,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/simulations/adapters" "github.com/ethereum/go-ethereum/swarm/api" "github.com/ethereum/go-ethereum/swarm/log" @@ -121,7 +137,7 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { //every node has a map to all nodes it had interactions //each entry in the map is a map of the other node with all the balances - balancesMap := make(map[discover.NodeID]map[discover.NodeID]*big.Int) + balancesMap := make(map[enode.ID]map[enode.ID]*big.Int) //iterate all nodes for _, node := range sim.NodeIDs() { @@ -133,7 +149,7 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { swarm := item.(*Swarm) //submap for each node is a map of all nodes with the balance for that node - subBalances := make(map[discover.NodeID]*big.Int) + subBalances := make(map[enode.ID]*big.Int) //iterate all nodes again... //get all balances with other peers for every node @@ -295,7 +311,7 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { } }) - balancesMap := make(map[discover.NodeID]map[discover.NodeID]*big.Int) + balancesMap := make(map[enode.ID]map[enode.ID]*big.Int) for _, node := range sim.NodeIDs() { item, ok := sim.NodeItem(node, bucketKeySwarm) @@ -305,7 +321,7 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { } swarm := item.(*Swarm) - subBalances := make(map[discover.NodeID]*big.Int) + subBalances := make(map[enode.ID]*big.Int) for _, n := range sim.NodeIDs() { if node == n { diff --git a/swarm/swarm.go b/swarm/swarm.go index 3a6c481fc0..fd6cbe242c 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -178,12 +178,6 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e if err := nodeID.UnmarshalText([]byte(config.NodeID)); err != nil { return nil, err } - if config.SwapEnabled { - self.swap, err = swap.New(stateStore) - if err != nil { - return nil, err - } - } self.streamer = stream.NewRegistry(nodeID, delivery, self.netStore, stateStore, &stream.RegistryOptions{ SkipCheck: config.DeliverySkipCheck, DoSync: config.SyncEnabled, From 1ef101b18b6038761a59fdc38177ab00eb86bca5 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Wed, 3 Oct 2018 17:07:53 -0500 Subject: [PATCH 19/30] swarm/swap: p2p protocol implementation --- p2p/protocols/protocol.go | 11 +++++++++++ swarm/swap/protocol.go | 1 - swarm/swap/swap.go | 30 ------------------------------ 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/p2p/protocols/protocol.go b/p2p/protocols/protocol.go index 615f74b569..59115933fc 100644 --- a/p2p/protocols/protocol.go +++ b/p2p/protocols/protocol.go @@ -141,11 +141,22 @@ type Spec struct { // each message must have a single unique data type Messages []interface{} + Services map[string]ProtocolService + initOnce sync.Once codes map[reflect.Type]uint64 types map[uint64]reflect.Type } +type ProtocolService interface { + AddServiceData(data interface{}) + IsSupported(key interface{}) +} + +func (s *Spec) RegisterProtocolService(key string, service ProtocolService) { + s.Services[key] = service +} + func (s *Spec) init() { s.initOnce.Do(func() { s.codes = make(map[reflect.Type]uint64, len(s.Messages)) diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go index 452160cd3a..36d5448e66 100644 --- a/swarm/swap/protocol.go +++ b/swarm/swap/protocol.go @@ -81,7 +81,6 @@ type RedeemChequeMsg struct { // SECTION: node.Service interface ///////////////////////////////////////////////////////////////////// func (s *Swap) Start(srv *p2p.Server) error { - s.registerForEvents(srv) log.Debug("Started swap") return nil } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index e346ce1706..63886d8ffb 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -163,36 +163,6 @@ func (dpo *DefaultPriceOracle) GetPriceForMsg(event *p2p.PeerEvent) (*big.Int, E return nil, false } -//This swap implementation works by listening to message events on the p2p server. -//It then handles the event received, filtering for messages and evaluating -//if it needs accounting -func (s *Swap) registerForEvents(srv *p2p.Server) { - go func() { - events := make(chan *p2p.PeerEvent) - sub := srv.SubscribeEvents(events) - defer sub.Unsubscribe() - - for { - select { - case event := <-events: - go s.handleMsgEvent(event) - case err := <-sub.Err(): - log.Error(err.Error()) - return - } - } - }() -} - -//Handle the message. -//Determine if it needs accounting, and if yes, account for it -func (s *Swap) handleMsgEvent(event *p2p.PeerEvent) { - if !s.priceOracle.IsAccountedMsg(event) { - return - } - s.accountMsgForPeer(event) -} - //Do the accounting //Depending on the charging type of the message (set in the PriceTag), //it will charge the sender or receiver From 4cd40f108f37ad5635f97e48860dd4b30ed074dc Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 4 Oct 2018 17:58:18 -0500 Subject: [PATCH 20/30] swarm/swap: first commit for p2p accounting --- p2p/protocols/accounting.go | 77 ++++ p2p/protocols/protocol.go | 32 +- swarm/network/stream/common_test.go | 2 +- swarm/network/stream/delivery_test.go | 4 +- swarm/network/stream/intervals_test.go | 2 +- swarm/network/stream/snapshot_sync_test.go | 6 +- swarm/network/stream/stream.go | 128 +++++-- swarm/network/stream/syncer_test.go | 2 +- swarm/swap/api.go | 16 +- swarm/swap/chequemanager.go | 8 +- swarm/swap/swap.go | 285 ++++----------- swarm/swap/swap_test.go | 386 +++++++-------------- swarm/swap_test.go | 238 +++++++------ swarm/swarm.go | 12 +- 14 files changed, 564 insertions(+), 634 deletions(-) create mode 100644 p2p/protocols/accounting.go diff --git a/p2p/protocols/accounting.go b/p2p/protocols/accounting.go new file mode 100644 index 0000000000..4c3e95e67b --- /dev/null +++ b/p2p/protocols/accounting.go @@ -0,0 +1,77 @@ +// Copyright 2018 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 protocols + +type PriceOracle interface { + Price(uint32, interface{}) (EntryDirection, uint64) + Accountable(interface{}) bool +} + +type BalanceManager interface { + //Credit is crediting the peer, charging local node + Credit(peer *Peer, amount uint64) error + //Debit is crediting the local node, charging the remote peer + Debit(peer *Peer, amount uint64) error +} + +type EntryDirection bool + +const ( + ChargeSender EntryDirection = true + ChargeReceiver EntryDirection = false +) + +type AccountingHook struct { + BalanceManager + PriceOracle +} + +func NewAccountingHook(mgr BalanceManager, po PriceOracle) *AccountingHook { + ah := &AccountingHook{ + PriceOracle: po, + BalanceManager: mgr, + } + return ah +} + +func (ah *AccountingHook) Send(peer *Peer, size uint32, msg interface{}) error { + var err error + if !ah.PriceOracle.Accountable(msg) { + return nil + } + direction, price := ah.PriceOracle.Price(size, msg) + if direction == ChargeSender { + err = ah.BalanceManager.Debit(peer, price) + } else { + err = ah.BalanceManager.Credit(peer, price) + } + return err +} + +func (ah *AccountingHook) Receive(peer *Peer, size uint32, msg interface{}) error { + var err error + if !ah.PriceOracle.Accountable(msg) { + return nil + } + direction, price := ah.PriceOracle.Price(size, msg) + if direction == ChargeReceiver { + err = ah.BalanceManager.Debit(peer, price) + } else { + err = ah.BalanceManager.Credit(peer, price) + } + return err +} diff --git a/p2p/protocols/protocol.go b/p2p/protocols/protocol.go index 59115933fc..b0e1c3a865 100644 --- a/p2p/protocols/protocol.go +++ b/p2p/protocols/protocol.go @@ -122,6 +122,11 @@ type WrappedMsg struct { Payload []byte } +type Hook interface { + Send(*Peer, uint32, interface{}) error + Receive(*Peer, uint32, interface{}) error +} + // Spec is a protocol specification including its name and version as well as // the types of messages which are exchanged type Spec struct { @@ -141,22 +146,13 @@ type Spec struct { // each message must have a single unique data type Messages []interface{} - Services map[string]ProtocolService + Hook Hook initOnce sync.Once codes map[reflect.Type]uint64 types map[uint64]reflect.Type } -type ProtocolService interface { - AddServiceData(data interface{}) - IsSupported(key interface{}) -} - -func (s *Spec) RegisterProtocolService(key string, service ProtocolService) { - s.Services[key] = service -} - func (s *Spec) init() { s.initOnce.Do(func() { s.codes = make(map[reflect.Type]uint64, len(s.Messages)) @@ -289,6 +285,15 @@ func (p *Peer) Send(ctx context.Context, msg interface{}) error { if !found { return errorf(ErrInvalidMsgType, "%v", code) } + + if p.spec.Hook != nil { + err := p.spec.Hook.Send(p, wmsg.Size, msg) + if err != nil { + p.Drop(err) + return err + } + } + return p2p.Send(p.rw, code, wmsg) } @@ -347,6 +352,13 @@ func (p *Peer) handleIncoming(handle func(ctx context.Context, msg interface{}) return errorf(ErrDecode, "<= %v: %v", msg, err) } + if p.spec.Hook != nil { + err := p.spec.Hook.Receive(p, wmsg.Size, val) + if err != nil { + return err + } + } + // call the registered handler callbacks // a registered callback take the decoded message as argument as an interface // which the handler is supposed to cast to the appropriate type diff --git a/swarm/network/stream/common_test.go b/swarm/network/stream/common_test.go index 72fdb2bd96..7b536db3fd 100644 --- a/swarm/network/stream/common_test.go +++ b/swarm/network/stream/common_test.go @@ -114,7 +114,7 @@ func newStreamerTester(t *testing.T, registryOptions *RegistryOptions) (*p2ptest delivery := NewDelivery(to, netStore) netStore.NewNetFetcherFunc = network.NewFetcherFactory(delivery.RequestFromPeers, true).New - streamer := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), registryOptions) + streamer := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), registryOptions, nil) teardown := func() { streamer.Close() removeDataDir() diff --git a/swarm/network/stream/delivery_test.go b/swarm/network/stream/delivery_test.go index c6ebae3f0e..f4fda4cb49 100644 --- a/swarm/network/stream/delivery_test.go +++ b/swarm/network/stream/delivery_test.go @@ -337,7 +337,7 @@ func testDeliveryFromNodes(t *testing.T, nodes, conns, chunkCount int, skipCheck r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), &RegistryOptions{ SkipCheck: skipCheck, - }) + }, nil) bucket.Store(bucketKeyRegistry, r) fileStore := storage.NewFileStore(netStore, storage.NewFileStoreParams()) @@ -525,7 +525,7 @@ func benchmarkDeliveryFromNodes(b *testing.B, nodes, conns, chunkCount int, skip SkipCheck: skipCheck, DoSync: true, SyncUpdateDelay: 0, - }) + }, nil) fileStore := storage.NewFileStore(netStore, storage.NewFileStoreParams()) bucket.Store(bucketKeyFileStore, fileStore) diff --git a/swarm/network/stream/intervals_test.go b/swarm/network/stream/intervals_test.go index 3164193b34..a0af67bf99 100644 --- a/swarm/network/stream/intervals_test.go +++ b/swarm/network/stream/intervals_test.go @@ -84,7 +84,7 @@ func testIntervals(t *testing.T, live bool, history *Range, skipCheck bool) { r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), &RegistryOptions{ SkipCheck: skipCheck, - }) + }, nil) bucket.Store(bucketKeyRegistry, r) r.RegisterClientFunc(externalStreamName, func(p *Peer, t string, live bool) (Client, error) { diff --git a/swarm/network/stream/snapshot_sync_test.go b/swarm/network/stream/snapshot_sync_test.go index 0d58494873..5d42341003 100644 --- a/swarm/network/stream/snapshot_sync_test.go +++ b/swarm/network/stream/snapshot_sync_test.go @@ -163,12 +163,10 @@ func streamerFunc(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Servic kad := network.NewKademlia(addr.Over(), network.NewKadParams()) delivery := NewDelivery(kad, netStore) netStore.NewNetFetcherFunc = network.NewFetcherFactory(dummyRequestFromPeers, true).New - r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), &RegistryOptions{ DoSync: true, SyncUpdateDelay: 3 * time.Second, - }) - + }, nil) bucket.Store(bucketKeyRegistry, r) cleanup = func() { @@ -358,7 +356,7 @@ func testSyncingViaDirectSubscribe(t *testing.T, chunkCount int, nodeCount int) delivery := NewDelivery(kad, netStore) netStore.NewNetFetcherFunc = network.NewFetcherFactory(dummyRequestFromPeers, true).New - r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), nil) + r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), nil, nil) bucket.Store(bucketKeyRegistry, r) fileStore := storage.NewFileStore(netStore, storage.NewFileStoreParams()) diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index fe975f40e4..12ff7120e5 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -61,6 +61,8 @@ type Registry struct { intervalsStore state.Store doRetrieve bool maxPeerServers int + balanceMgr protocols.BalanceManager + priceOracle protocols.PriceOracle } // RegistryOptions holds optional values for NewRegistry constructor. @@ -73,7 +75,7 @@ type RegistryOptions struct { } // NewRegistry is Streamer constructor -func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.SyncChunkStore, intervalsStore state.Store, options *RegistryOptions) *Registry { +func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.SyncChunkStore, intervalsStore state.Store, options *RegistryOptions, balanceMgr protocols.BalanceManager) *Registry { if options == nil { options = &RegistryOptions{} } @@ -180,6 +182,9 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy }() } + streamer.balanceMgr = balanceMgr + streamer.priceOracle = streamer.createPriceOracle() + return streamer } @@ -448,7 +453,7 @@ func (r *Registry) updateSyncing() { } func (r *Registry) runProtocol(p *p2p.Peer, rw p2p.MsgReadWriter) error { - peer := protocols.NewPeer(p, rw, Spec) + peer := protocols.NewPeer(p, rw, r.GetSpec()) bp := network.NewBzzPeer(peer) np := network.NewPeer(bp, r.delivery.kad) r.delivery.kad.On(np) @@ -672,32 +677,109 @@ func (c *clientParams) clientCreated() { close(c.clientCreatedC) } -// Spec is the spec of the streamer protocol -var Spec = &protocols.Spec{ - Name: "stream", - Version: 7, - MaxMsgSize: 10 * 1024 * 1024, - Messages: []interface{}{ - UnsubscribeMsg{}, - OfferedHashesMsg{}, - WantedHashesMsg{}, - TakeoverProofMsg{}, - SubscribeMsg{}, - RetrieveRequestMsg{}, - ChunkDeliveryMsgRetrieval{}, - SubscribeErrorMsg{}, - RequestSubscriptionMsg{}, - QuitMsg{}, - ChunkDeliveryMsgSyncing{}, - }, +func (r *Registry) GetSpec() *protocols.Spec { + + // Spec is the spec of the streamer protocol + var spec = &protocols.Spec{ + Name: "stream", + Version: 7, + MaxMsgSize: 10 * 1024 * 1024, + Messages: []interface{}{ + UnsubscribeMsg{}, + OfferedHashesMsg{}, + WantedHashesMsg{}, + TakeoverProofMsg{}, + SubscribeMsg{}, + RetrieveRequestMsg{}, + ChunkDeliveryMsgRetrieval{}, + SubscribeErrorMsg{}, + RequestSubscriptionMsg{}, + QuitMsg{}, + ChunkDeliveryMsgSyncing{}, + }, + Hook: protocols.NewAccountingHook(r.balanceMgr, r.priceOracle), + } + return spec +} + +//An accountable message needs some meta information attached to it +//in order to evaluate the correct price +type priceTag struct { + price uint64 + sizeBased bool + direction protocols.EntryDirection +} +type StreamerPriceOracle struct { + priceMatrix map[uint64]*priceTag + registry *Registry +} + +func (spo *StreamerPriceOracle) Accountable(msg interface{}) bool { + code, ok := spo.registry.GetSpec().GetCode(msg) + if !ok { + return false + } + if _, ok = spo.priceMatrix[code]; ok { + return true + } + return false +} + +func (spo *StreamerPriceOracle) Price(size uint32, msg interface{}) (direction protocols.EntryDirection, price uint64) { + code, ok := spo.registry.GetSpec().GetCode(msg) + if !ok { + panic("Attempting to get message code for an expected message type, but code not found") + } + + tag := spo.priceMatrix[code] + price = tag.price + if tag.sizeBased { + price = price * uint64(size) + } + direction = tag.direction + return +} + +func (r *Registry) createPriceOracle() protocols.PriceOracle { + + po := &StreamerPriceOracle{ + registry: r, + } + po.priceMatrix = make(map[uint64]*priceTag) + + deliveryCode, ok := r.GetSpec().GetCode(ChunkDeliveryMsgRetrieval{}) + if !ok { + panic("Attempting to get message code for an expected message type, but code not found") + } + tag := &priceTag{ + price: uint64(100), + sizeBased: true, + direction: protocols.ChargeReceiver, + } + po.priceMatrix[deliveryCode] = tag + + retrieveReqCode, ok := r.GetSpec().GetCode(RetrieveRequestMsg{}) + if !ok { + panic("Attempting to get message code for an expected message type, but code not found") + } + tag = &priceTag{ + price: uint64(10), + sizeBased: false, + direction: protocols.ChargeSender, + } + po.priceMatrix[retrieveReqCode] = tag + + return po + } func (r *Registry) Protocols() []p2p.Protocol { + spec := r.GetSpec() return []p2p.Protocol{ { - Name: Spec.Name, - Version: Spec.Version, - Length: Spec.Length(), + Name: spec.Name, + Version: spec.Version, + Length: spec.Length(), Run: r.runProtocol, // NodeInfo: , // PeerInfo: , diff --git a/swarm/network/stream/syncer_test.go b/swarm/network/stream/syncer_test.go index f2be3bef99..eb982c9a1e 100644 --- a/swarm/network/stream/syncer_test.go +++ b/swarm/network/stream/syncer_test.go @@ -115,7 +115,7 @@ func testSyncBetweenNodes(t *testing.T, nodes, conns, chunkCount int, skipCheck r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), &RegistryOptions{ SkipCheck: skipCheck, - }) + }, nil) fileStore := storage.NewFileStore(netStore, storage.NewFileStoreParams()) bucket.Store(bucketKeyFileStore, fileStore) diff --git a/swarm/swap/api.go b/swarm/swap/api.go index 15ace353dc..efdd94f092 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -19,7 +19,6 @@ package swap import ( "context" "errors" - "math/big" "github.com/ethereum/go-ethereum/p2p/enode" ) @@ -44,9 +43,10 @@ func NewAPI(swap *Swap) *API { } //Get the balance for this node with a specific peer -func (swapapi *API) BalanceWithPeer(ctx context.Context, peer enode.ID) (balance *big.Int, err error) { - balance = swapapi.swap.peers[peer] - if balance == nil { +func (swapapi *API) BalanceWithPeer(ctx context.Context, peer enode.ID) (balance int64, err error) { + var ok bool + balance, ok = swapapi.swap.balances[peer] + if !ok { err = ErrNoSuchPeerAccounting } return @@ -56,10 +56,10 @@ func (swapapi *API) BalanceWithPeer(ctx context.Context, peer enode.ID) (balance //Iterates over all peers this node is having accounted interaction //and just adds up balances. //It assumes that if a disfavorable balance is represented as a negative value -func (swapapi *API) Balance(ctx context.Context) (balance *big.Int, err error) { - balance = big.NewInt(0) - for _, peerBalance := range swapapi.swap.peers { - balance.Add(balance, peerBalance) +func (swapapi *API) Balance(ctx context.Context) (balance int64, err error) { + balance = 0 + for _, peerBalance := range swapapi.swap.balances { + balance += peerBalance } return } diff --git a/swarm/swap/chequemanager.go b/swarm/swap/chequemanager.go index 1fb2029d72..8e272a6426 100644 --- a/swarm/swap/chequemanager.go +++ b/swarm/swap/chequemanager.go @@ -17,8 +17,6 @@ package swap import ( - "math/big" - "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/swarm/state" ) @@ -33,8 +31,8 @@ type ChequeManager struct { type Cheque struct { serial uint64 timeout uint64 - amount *big.Int - sumCumulated *big.Int + amount int64 + sumCumulated int64 beneficiary enode.ID //this should probably be common.Address? } @@ -48,7 +46,7 @@ func NewChequeManager(stateStore state.Store) *ChequeManager { } } -func (mgr *ChequeManager) CreateCheque(beneficiary enode.ID, amount *big.Int) *Cheque { +func (mgr *ChequeManager) CreateCheque(beneficiary enode.ID, amount int64) *Cheque { mgr.serialPerNode[beneficiary]++ cheque := &Cheque{ serial: mgr.serialPerNode[beneficiary], diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 63886d8ffb..fd5a334fb6 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -20,25 +20,26 @@ import ( "context" "errors" "fmt" - "math/big" + "math" + "strconv" "sync" - "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/swarm/log" - "github.com/ethereum/go-ethereum/swarm/network/stream" "github.com/ethereum/go-ethereum/swarm/state" ) const ( defaultMaxMsgSize = 1024 * 1024 + OracleID = "swap" swapProtocolName = "swap" swapVersion = 1 ) var ( - payAt = big.NewInt(-4096 * 10000) // threshold that triggers payment {request} (bytes) - dropAt = big.NewInt(-4096 * 12000) // threshold that triggers disconnect (bytes) + payAt = int64(-4096 * 10000) // threshold that triggers payment {request} (bytes) + dropAt = int64(-4096 * 12000) // threshold that triggers disconnect (bytes) ErrNotAccountedMsg = errors.New("Message does not need accounting") ErrInsufficientFunds = errors.New("Insufficient funds") @@ -49,252 +50,116 @@ var ( // A node maintains an individual balance with every peer // Only messages which have a price will be accounted for type Swap struct { - priceOracle PriceOracle //the price oracle facilitates message pricing to the swap instance - chequeManager *ChequeManager //cheque manager keeps track of issued cheques - stateStore state.Store //stateStore is needed in order to keep balances across sessions - lock sync.RWMutex //lock the balances - peers map[enode.ID]*big.Int //map of balances for each peer - protocol *Protocol //reference to the cheque exchange protocol -} - -//PriceOracle is responsible to maintain the price matrix for accounted messages, -//to get its price and to evaluate if a message is accountable or not -type PriceOracle interface { - IsAccountedMsg(event *p2p.PeerEvent) bool - GetPriceForMsg(event *p2p.PeerEvent) (*big.Int, EntryDirection) -} - -//This defines if a price will be debited or credited to an account -type EntryDirection bool - -//For some messages the sender pays (e.g. RetrieveRequestMsg), -//for others the receiver (ChunkDeliveryMsg) -const ( - ChargeSender EntryDirection = true - ChargeReceiver EntryDirection = false -) - -//An accountable message needs some meta information attached to it -//in order to evaluate the correct price -type PriceTag struct { - Price *big.Int - SizeBased bool - Direction EntryDirection -} - -//The default price oracle -type DefaultPriceOracle struct { - priceMatrix map[string]map[uint64]*PriceTag -} - -//Load a default price matrix -func (dpo *DefaultPriceOracle) LoadPriceMatrix() { - dpo.priceMatrix = dpo.loadDefaultPriceMatrix() -} - -//Set up a default price matrix -//This statically builds the price matrix according to previous knowledge -//of which messages need accounting -//The matrix can be queried by -// * the protocol string, returns a sub-map -// * the sub-map by the message code, which returns the PriceTag -func (dpo *DefaultPriceOracle) loadDefaultPriceMatrix() map[string]map[uint64]*PriceTag { - //setup matrix - priceMatrix := make(map[string]map[uint64]*PriceTag) - - //define accounted messages from the stream protocol - streamProtocol := stream.Spec.Name - priceMatrix[streamProtocol] = make(map[uint64]*PriceTag) - - //get indexes of accounted messages in order to populate the map - retrieveRequestMsgIndex, ok := stream.Spec.GetCode(stream.RetrieveRequestMsg{}) - if !ok { - log.Crit("setting up price matrix failed; expected message code not found in Spec!") - return nil - } - deliveryMsgIndex, ok := stream.Spec.GetCode(stream.RetrieveRequestMsg{}) - if !ok { - log.Crit("setting up price matrix failed; expected message code not found in Spec!") - return nil - } - - //assign the price tag to the message - priceMatrix[streamProtocol][uint64(retrieveRequestMsgIndex)] = &PriceTag{ - Price: big.NewInt(10), - SizeBased: false, - Direction: ChargeSender, - } - - priceMatrix[streamProtocol][uint64(deliveryMsgIndex)] = &PriceTag{ - Price: big.NewInt(100), - SizeBased: true, - Direction: ChargeReceiver, - } - - return priceMatrix - -} - -//Check if a message needs accounting -func (dpo *DefaultPriceOracle) IsAccountedMsg(event *p2p.PeerEvent) bool { - var protoPriceMap map[uint64]*PriceTag - var ok bool - if protoPriceMap, ok = dpo.priceMatrix[event.Protocol]; !ok { - return false - } - if _, ok = protoPriceMap[*event.MsgCode]; !ok { - return false - } - return true -} - -//Get the actual price of a message -//If the message is size-based, it returns the calculated price -func (dpo *DefaultPriceOracle) GetPriceForMsg(event *p2p.PeerEvent) (*big.Int, EntryDirection) { - if dpo.IsAccountedMsg(event) { - priceTag := dpo.priceMatrix[event.Protocol][*event.MsgCode] - price := &big.Int{} - price.Set(priceTag.Price) - if priceTag.SizeBased { - price.Mul(priceTag.Price, big.NewInt(int64(*event.MsgSize))) - } - return price, priceTag.Direction - } - return nil, false -} - -//Do the accounting -//Depending on the charging type of the message (set in the PriceTag), -//it will charge the sender or receiver -func (s *Swap) accountMsgForPeer(event *p2p.PeerEvent) { - price, direction := s.priceOracle.GetPriceForMsg(event) - if price == nil { - //TODO what to do in this case? Should not happen - panic("Price is nil; this should have been accounted for but somehow it failed") - } - - if chargeLocal := (direction == ChargeSender) == (event.Type == p2p.PeerEventTypeMsgSend); chargeLocal { - //debit local node and credit remote - s.chargeLocal(event, price) - } else { - //credit local node and debit remote - s.chargeRemote(event, price) - } -} - -//Debit us and credit remote -func (s *Swap) chargeLocal(event *p2p.PeerEvent, amount *big.Int) { - s.lock.Lock() - defer s.lock.Unlock() - //local node is being credited (in its favor), so its balance increases - if s.peers[event.Peer] == nil { - s.peers[event.Peer] = &big.Int{} - s.stateStore.Get(event.Peer.String(), s.peers[event.Peer]) - } - peerBalance := s.peers[event.Peer] - peerBalance.Sub(peerBalance, amount) - //local node is being debited (in favor of remote peer), so its balance decreases - //TODO: save to store here? init store? - s.stateStore.Put(event.Peer.String(), peerBalance) - //(balance *Int) Cmp(payAt) - // -1 if balance < payAt - // 0 if balance == payAt - // +1 if balance > payAt - if peerBalance.Cmp(payAt) == -1 { - ctx := context.TODO() - err := s.issueCheque(ctx, event.Peer) - if err != nil { - //TODO: special error handling, as at this point the accounting has been done - //but the cheque could not be sent? - log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) - } - } - if peerBalance.Cmp(dropAt) == -1 { - peer := s.protocol.getPeer(event.Peer) - if peer != nil { - peer.Drop(ErrInsufficientFunds) - //this little hack allows for tests to verify that this error occurred - event.Error = ErrInsufficientFunds.Error() - } - } - log.Debug(fmt.Sprintf("balance for peer %s: %s", event.Peer.String(), peerBalance.String())) + chequeManager *ChequeManager //cheque manager keeps track of issued cheques + stateStore state.Store //stateStore is needed in order to keep balances across sessions + lock sync.RWMutex //lock the balances + balances map[enode.ID]int64 //map of balances for each peer + protocol *Protocol //reference to the cheque exchange protocol } //Credit us and debit remote -func (s *Swap) chargeRemote(event *p2p.PeerEvent, amount *big.Int) { +func (s *Swap) Credit(peer *protocols.Peer, amount uint64) (err error) { s.lock.Lock() defer s.lock.Unlock() - //local node is being credited (in its favor), so its balance increases - if s.peers[event.Peer] == nil { - s.peers[event.Peer] = &big.Int{} + + peerBalance := s.balances[peer.ID()] + if _, ok := s.balances[peer.ID()]; !ok { + s.stateStore.Get(peer.ID().String(), &peerBalance) + s.balances[peer.ID()] = peerBalance } - peerBalance := s.peers[event.Peer] - peerBalance.Add(peerBalance, amount) - //local node is being credited(in favor of local peer), so its balance increases - //TODO: save to store here? init store? - s.stateStore.Put(event.Peer.String(), peerBalance) - //(balance *Int) Cmp(payAt) - // -1 if balance < payAt - // 0 if balance == payAt - // +1 if balance > payAt - if peerBalance.Cmp(payAt) == -1 { + //local node is being credited(in favor of local node), so the balance increases + s.balances[peer.ID()] += int64(amount) + peerBalance = s.balances[peer.ID()] + s.stateStore.Put(peer.ID().String(), &peerBalance) + + if float64(peerBalance) > math.Abs(float64(payAt)) { ctx := context.TODO() - err := s.issueCheque(ctx, event.Peer) + err := s.wantCheque(ctx, peer.ID()) if err != nil { //TODO: special error handling, as at this point the accounting has been done //but the cheque could not be sent? log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) } } - if peerBalance.Cmp(dropAt) == -1 { - peer := s.protocol.getPeer(event.Peer) - if peer != nil { - //this little hack allows for tests to verify that this error occurred - peer.Drop(ErrInsufficientFunds) + if float64(peerBalance) > math.Abs(float64(dropAt)) { + return ErrInsufficientFunds + } + log.Debug(fmt.Sprintf("balance for peer %s: %s", peer.ID().String(), strconv.FormatInt(peerBalance, 10))) + return err +} + +//Debit us and credit remote +func (s *Swap) Debit(peer *protocols.Peer, amount uint64) (err error) { + s.lock.Lock() + defer s.lock.Unlock() + + peerBalance := s.balances[peer.ID()] + if _, ok := s.balances[peer.ID()]; !ok { + s.stateStore.Get(peer.ID().String(), &peerBalance) + s.balances[peer.ID()] = peerBalance + } + //local node is being debited (in favor of remote peer), so its balance decreases + s.balances[peer.ID()] -= int64(amount) + peerBalance = s.balances[peer.ID()] + s.stateStore.Put(peer.ID().String(), &peerBalance) + if peerBalance < payAt { + ctx := context.TODO() + err := s.issueCheque(ctx, peer.ID()) + if err != nil { + //TODO: special error handling, as at this point the accounting has been done + //but the cheque could not be sent? + log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) } } - log.Debug(fmt.Sprintf("balance for peer %s: %s", event.Peer.String(), peerBalance.String())) + if peerBalance < dropAt { + return ErrInsufficientFunds + } + log.Debug(fmt.Sprintf("balance for peer %s: %s", peer.ID().String(), strconv.FormatInt(peerBalance, 10))) + return nil } //get a peer's balance -func (swap *Swap) GetPeerBalance(peer enode.ID) *big.Int { +func (swap *Swap) GetPeerBalance(peer enode.ID) (int64, error) { swap.lock.RLock() defer swap.lock.RUnlock() - if p, ok := swap.peers[peer]; ok { - return p + if p, ok := swap.balances[peer]; ok { + return p, nil } - return nil + return 0, errors.New("Peer not found") } //Issue a cheque for the remote peer. Happens if we are indebted with the peer //and crossed the payment threshold func (s *Swap) issueCheque(ctx context.Context, id enode.ID) error { - amount := &big.Int{} - cheque := s.chequeManager.CreateCheque(id, amount.Abs(payAt)) - msg := IssueChequeMsg{ + cheque := s.chequeManager.CreateCheque(id, int64(math.Abs(float64(payAt)))) + _ = IssueChequeMsg{ Cheque: cheque, } - //TODO: This should now be via the actual SwapProtocol p := s.protocol.getPeer(id) if p == nil { return fmt.Errorf("wanting to send to non-connected peer!") } - return p.Send(ctx, msg) + return nil + //TODO: Don't actually send any cheques yet + //return p.Send(ctx, msg) +} + +//Issue a cheque for the remote peer. Happens if we are indebted with the peer +//and crossed the payment threshold +func (s *Swap) wantCheque(ctx context.Context, id enode.ID) error { + //TODO: Don't actually initially any real message exchange yet + //return p.Send(ctx, msg) + return nil } // New - swap constructor func New(stateStore state.Store) (swap *Swap) { - priceOracle := &DefaultPriceOracle{} - swap = &Swap{ chequeManager: NewChequeManager(stateStore), stateStore: stateStore, - peers: make(map[enode.ID]*big.Int), - priceOracle: priceOracle, + balances: make(map[enode.ID]int64), protocol: NewProtocol(), } - priceOracle.LoadPriceMatrix() - return } diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index c731745342..f2b460d92d 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -18,10 +18,11 @@ package swap import ( "context" + "crypto/rand" "flag" "fmt" "io/ioutil" - "math/big" + "math" "os" "path/filepath" "testing" @@ -32,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/p2p/simulations/adapters" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/swarm/state" colorable "github.com/mattn/go-colorable" @@ -44,6 +46,29 @@ var ( loglevel = flag.Int("loglevel", 2, "verbosity of logs") ) +type testPriceOracle struct{} + +func (tpo *testPriceOracle) Accountable(msg interface{}) bool { + _, ok := testSpec.GetCode(msg) + return ok +} + +func (tpo *testPriceOracle) Price(size uint32, msg interface{}) (protocols.EntryDirection, uint64) { + switch msg := msg.(type) { + case *testExceedsPayAtMsg: + return msg.Price(size) + case *testExceedsDropAtMsg: + return msg.Price(size) + case *testCheapMsg: + return msg.Price(size) + case *testSizeBasedMsg: + return msg.Price(size) + case *testChargeRecvMsg: + return msg.Price(size) + } + return false, 0 +} + var testSpec = &protocols.Spec{ Name: "swapTestSpec", Version: 1, @@ -79,36 +104,34 @@ func (d *dummyRW) ReadMsg() (p2p.Msg, error) { type testExceedsPayAtMsg struct{} type testExceedsDropAtMsg struct{} type testCheapMsg struct{} -type testSizeBasedMsg struct{} +type testSizeBasedMsg struct { + Data []byte +} type testChargeRecvMsg struct{} //this message is just one unit more expensive than the payment threshold -func (tmsg *testExceedsPayAtMsg) Price() *big.Int { - diff := &big.Int{} - diff = diff.Abs(payAt) - return diff.Add(diff, big.NewInt(1)) +func (tmsg *testExceedsPayAtMsg) Price(size uint32) (protocols.EntryDirection, uint64) { + return protocols.ChargeSender, uint64(math.Abs(float64(payAt)) + float64(1)) } //this message is just one unit more expensive than the disconnect threshold -func (tmsg *testExceedsDropAtMsg) Price() *big.Int { - diff := &big.Int{} - diff = diff.Abs(dropAt) - return diff.Add(diff, big.NewInt(1)) +func (tmsg *testExceedsDropAtMsg) Price(size uint32) (protocols.EntryDirection, uint64) { + return protocols.ChargeSender, uint64(math.Abs(float64(dropAt)) + float64(1)) } //a message with an arbitrary cost -func (tmsg *testCheapMsg) Price() *big.Int { - return big.NewInt(100) +func (tmsg *testCheapMsg) Price(size uint32) (protocols.EntryDirection, uint64) { + return protocols.ChargeSender, uint64(10) } //a message which needs to be charged based on price -func (tmsg *testSizeBasedMsg) Price() *big.Int { - return big.NewInt(222) +func (tmsg *testSizeBasedMsg) Price(size uint32) (protocols.EntryDirection, uint64) { + return protocols.ChargeSender, uint64(size) * uint64(100) } //a message which needs to be charged based on price -func (tmsg *testChargeRecvMsg) Price() *big.Int { - return big.NewInt(999) +func (tmsg *testChargeRecvMsg) Price(size uint32) (protocols.EntryDirection, uint64) { + return protocols.ChargeReceiver, uint64(999) } func init() { @@ -120,8 +143,8 @@ func init() { //check that the disconnect threshold is below the payment threshold func TestLimits(t *testing.T) { - if dropAt.Cmp(payAt) > -1 { - t.Fatal(fmt.Sprintf("dropAt limit is not lower than payAt limit, dropAt: %s, payAt: %s", dropAt.String(), payAt.String())) + if dropAt >= payAt { + t.Fatal(fmt.Sprintf("dropAt limit is not lower than payAt limit, dropAt: %d, payAt: %d", dropAt, payAt)) } } @@ -136,32 +159,18 @@ func TestExceedsPayAt(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - swap.priceOracle = setupPriceMatrix() + msg := &testExceedsPayAtMsg{} + ctx := context.Background() + testPeer := newDummyPeer() + testPeer.Send(ctx, msg) - node := createAndStartSvcNode(swap, t) - defer node.Stop() - defer os.RemoveAll(node.DataDir()) - - peerID := adapters.RandomNodeConfig().ID - - code, ok := testSpec.GetCode(&testExceedsPayAtMsg{}) - if !ok { - t.Fatal("test message not found in spec") + _, price := msg.Price(0) + if swap.balances[testPeer.ID()] != int64(0-price) { + t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) } - event := &p2p.PeerEvent{ - Type: p2p.PeerEventTypeMsgSend, - Protocol: testSpec.Name, - Peer: peerID, - MsgCode: &code, - MsgSize: new(uint32), - Error: "", - } - - swap.handleMsgEvent(event) - - //check that a cheque is present - cheques := swap.chequeManager.openDebitCheques[peerID] + //check that a cheque has been created + cheques := swap.chequeManager.openDebitCheques[testPeer.ID()] if cheques == nil { t.Fatal("Expected cheques for this peer to be present, but are nil") } @@ -171,9 +180,8 @@ func TestExceedsPayAt(t *testing.T) { if cheques[0].serial != 1 { t.Fatal(fmt.Sprintf("Expected the serial to be one (first message but is: %d", cheques[0].serial)) } - absPayAt := big.NewInt(0) - if cheques[0].amount.Cmp(absPayAt.Abs(payAt)) != 0 { - t.Fatal(fmt.Sprintf("Expected the serial to be one (first message but is: %d", cheques[0].serial)) + if float64(cheques[0].amount) != float64(math.Abs(float64(payAt))) { + t.Fatal(fmt.Sprintf("Expected cheques amount to be equal to payAt limit, but it is: %d", cheques[0].amount)) } } @@ -185,79 +193,37 @@ func TestExceedsDropAt(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - swap.priceOracle = setupPriceMatrix() + msg := &testExceedsDropAtMsg{} + ctx := context.Background() + testPeer := newDummyPeer() + err := testPeer.Send(ctx, msg) - node := createAndStartSvcNode(swap, t) - defer node.Stop() - defer os.RemoveAll(node.DataDir()) - - //peerID := adapters.RandomNodeConfig().ID - peer := newDummyPeer() - swap.protocol.setPeer(peer.Peer) - - code, ok := testSpec.GetCode(&testExceedsDropAtMsg{}) - if !ok { - t.Fatal("test message not found in spec") + _, price := msg.Price(0) + if swap.balances[testPeer.ID()] != int64(0-price) { + t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) } - event := &p2p.PeerEvent{ - Type: p2p.PeerEventTypeMsgSend, - Protocol: testSpec.Name, - Peer: peer.ID(), - MsgCode: &code, - MsgSize: new(uint32), - Error: "", - } - - swap.handleMsgEvent(event) - if event.Error != ErrInsufficientFunds.Error() { - t.Fatal("Excpected this test to fail with insufficient funds, but it did not") + if err != ErrInsufficientFunds { + t.Fatal("Expected this test to fail with insufficient funds, but it did not") } } //send a message with cost, //then check that the balance has the expected amount func TestSendCheapMessage(t *testing.T) { - fmt.Println("send") + //create a test swap account swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - swap.priceOracle = setupPriceMatrix() - - node := createAndStartSvcNode(swap, t) - defer node.Stop() - defer os.RemoveAll(node.DataDir()) - - //peerID := adapters.RandomNodeConfig().ID - peer := newDummyPeer() - swap.protocol.setPeer(peer.Peer) - //set an arbitrary test balance value - testBalance := big.NewInt(1234567890) - swap.peers[peer.ID()] = testBalance - - code, ok := testSpec.GetCode(&testCheapMsg{}) - if !ok { - t.Fatal("test message not found in spec") - } - - event := &p2p.PeerEvent{ - Type: p2p.PeerEventTypeMsgSend, - Protocol: testSpec.Name, - Peer: peer.ID(), - MsgCode: &code, - MsgSize: new(uint32), - Error: "", - } - - swap.handleMsgEvent(event) - - peerBalance := swap.peers[peer.ID()] msg := &testCheapMsg{} + ctx := context.Background() + testPeer := newDummyPeer() + testPeer.Send(ctx, msg) //check the new balance - if peerBalance.Cmp(testBalance.Sub(testBalance, msg.Price())) != 0 { - t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", - testBalance.Sub(testBalance, msg.Price()).String(), peerBalance.String())) + _, price := msg.Price(0) + if swap.balances[testPeer.ID()] != int64(0-price) { + t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) } } @@ -268,48 +234,45 @@ func TestSendCheapMessage(t *testing.T) { //then create a different SwapPeer instance with same peerID, //which will try to load a balance from the stateStore func TestRestoreBalanceFromStateStore(t *testing.T) { + //create a test swap account swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - swap.priceOracle = setupPriceMatrix() + msg := &testCheapMsg{} + ctx := context.Background() + testPeer := newDummyPeer() + testPeer.Send(ctx, msg) - node := createAndStartSvcNode(swap, t) - defer node.Stop() - defer os.RemoveAll(node.DataDir()) - - //peerID := adapters.RandomNodeConfig().ID - peer := newDummyPeer() - swap.protocol.setPeer(peer.Peer) - //create a reference an arbitrary balance - testBalance := big.NewInt(1234567890) - //assign the same value to the peer - swap.peers[peer.ID()] = big.NewInt(1234567890) - - code, ok := testSpec.GetCode(&testCheapMsg{}) - if !ok { - t.Fatal("test message not found in spec") + //check the new balance + _, price := msg.Price(0) + expectedBalance := int64(0 - price) + if swap.balances[testPeer.ID()] != expectedBalance { + t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) } - event := &p2p.PeerEvent{ - Type: p2p.PeerEventTypeMsgSend, - Protocol: testSpec.Name, - Peer: peer.ID(), - MsgCode: &code, - MsgSize: new(uint32), - Error: "", + var tmpBalance int64 + swap.stateStore.Get(testPeer.ID().String(), &tmpBalance) + //compare the balances + if expectedBalance != tmpBalance { + t.Fatal(fmt.Sprintf("Unexpected balance value in stateStore after sending cheap message test - stateStore should have saved it. Expected balance: %d, balance is: %d", + expectedBalance, tmpBalance)) } - swap.handleMsgEvent(event) + swap.stateStore.Close() + swap.stateStore = nil - peerBalance := swap.peers[peer.ID()] - expectedBalance := &big.Int{} - swap.stateStore.Get(peer.ID().String(), expectedBalance) + stateStore, err := state.NewDBStore(testDir) + if err != nil { + t.Fatal(err) + } + + var newBalance int64 + stateStore.Get(testPeer.ID().String(), &newBalance) //compare the balances - expectedBalance.Sub(testBalance, (&testCheapMsg{}).Price()) - if peerBalance.Cmp(expectedBalance) != 0 { - t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", - expectedBalance.String(), peerBalance.String())) + if expectedBalance != newBalance { + t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %d, balance is: %d", + expectedBalance, newBalance)) } } @@ -317,49 +280,32 @@ func TestRestoreBalanceFromStateStore(t *testing.T) { //then check that the balance has the expected amount //the message is charged size based func TestSendSizeBasedMsg(t *testing.T) { + //create a test swap account swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - swap.priceOracle = setupPriceMatrix() - - node := createAndStartSvcNode(swap, t) - defer node.Stop() - defer os.RemoveAll(node.DataDir()) - - peer := newDummyPeer() - swap.protocol.setPeer(peer.Peer) - //set an arbitrary test balance value - testBalance := big.NewInt(1234567890) - swap.peers[peer.ID()] = testBalance - - code, ok := testSpec.GetCode(&testSizeBasedMsg{}) - if !ok { - t.Fatal("test message not found in spec") - } - - size := new(uint32) - *size = 500 - - event := &p2p.PeerEvent{ - Type: p2p.PeerEventTypeMsgSend, - Protocol: testSpec.Name, - Peer: peer.ID(), - MsgCode: &code, - MsgSize: size, - Error: "", - } - - swap.handleMsgEvent(event) - - peerBalance := swap.peers[peer.ID()] msg := &testSizeBasedMsg{} - expectedBalance := &big.Int{} - expectedBalance.Mul(msg.Price(), big.NewInt(int64(*size))) + msg.Data = make([]byte, 16) + rand.Read(msg.Data) + ctx := context.Background() + testPeer := newDummyPeer() + testPeer.Send(ctx, msg) //check the new balance - if peerBalance.Cmp(testBalance.Sub(testBalance, expectedBalance)) != 0 { - t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", - expectedBalance.String(), peerBalance.String())) + r, err := rlp.EncodeToBytes(msg) + if err != nil { + t.Fatal(err) + } + + size := uint32(len(r)) + _, price := msg.Price(size) + expectedBalance := int64(0 - price) + expectedPrice := uint64(size * 100) + if price != expectedPrice { + t.Fatalf("Expected price to be %d, but is %d", expectedPrice, price) + } + if swap.balances[testPeer.ID()] != expectedBalance { + t.Fatalf("Expected balance to be %d, but is %d", expectedBalance, swap.balances[testPeer.ID()]) } } @@ -368,45 +314,19 @@ func TestSendSizeBasedMsg(t *testing.T) { //it means that the balance is changed in positive, //instead of negative as with all other tests func TestChargeAsReceiver(t *testing.T) { + //create a test swap account swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - swap.priceOracle = setupPriceMatrix() - - node := createAndStartSvcNode(swap, t) - defer node.Stop() - defer os.RemoveAll(node.DataDir()) - - //peerID := adapters.RandomNodeConfig().ID - peer := newDummyPeer() - swap.protocol.setPeer(peer.Peer) - //set an arbitrary test balance value - testBalance := big.NewInt(1234567890) - swap.peers[peer.ID()] = testBalance - - code, ok := testSpec.GetCode(&testChargeRecvMsg{}) - if !ok { - t.Fatal("test message not found in spec") - } - - event := &p2p.PeerEvent{ - Type: p2p.PeerEventTypeMsgSend, - Protocol: testSpec.Name, - Peer: peer.ID(), - MsgCode: &code, - MsgSize: new(uint32), - Error: "", - } - - swap.handleMsgEvent(event) - - peerBalance := swap.peers[peer.ID()] msg := &testChargeRecvMsg{} + ctx := context.Background() + testPeer := newDummyPeer() + testPeer.Send(ctx, msg) //check the new balance - if peerBalance.Cmp(testBalance.Add(testBalance, msg.Price())) != 0 { - t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %s, balance is: %s", - testBalance.Add(testBalance, msg.Price()).String(), peerBalance.String())) + _, price := msg.Price(0) + if swap.balances[testPeer.ID()] != int64(price) { + t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) } } @@ -422,44 +342,10 @@ func createTestSwap(t *testing.T) (*Swap, string) { t.Fatal(err2) } swap := New(stateStore) + testSpec.Hook = protocols.NewAccountingHook(swap, &testPriceOracle{}) return swap, dir } -func setupPriceMatrix() PriceOracle { - - priceOracle := &DefaultPriceOracle{} - priceOracle.priceMatrix = make(map[string]map[uint64]*PriceTag) - priceOracle.priceMatrix[testSpec.Name] = make(map[uint64]*PriceTag) - protoMatrix := priceOracle.priceMatrix[testSpec.Name] - protoMatrix[0] = &PriceTag{ - Direction: ChargeSender, - Price: (&testExceedsPayAtMsg{}).Price(), - } - - protoMatrix[1] = &PriceTag{ - Direction: ChargeSender, - Price: (&testExceedsDropAtMsg{}).Price(), - } - - protoMatrix[2] = &PriceTag{ - Direction: ChargeSender, - Price: (&testCheapMsg{}).Price(), - } - - protoMatrix[3] = &PriceTag{ - Direction: ChargeSender, - Price: (&testSizeBasedMsg{}).Price(), - SizeBased: true, - } - - protoMatrix[4] = &PriceTag{ - Direction: ChargeReceiver, - Price: (&testChargeRecvMsg{}).Price(), - } - - return priceOracle -} - //dummy message handler (needed or we will have a panic in the accounting) func dummyMsgHandler(ctx context.Context, msg interface{}) error { return nil @@ -506,14 +392,14 @@ func TestSwapRPC(t *testing.T) { } defer os.RemoveAll(stack.DataDir()) - var balance *big.Int + var balance int64 err = rpcclient.Call(&balance, "swap_balance") if err != nil { t.Fatal("servicenode RPC failed", "err", err) } log.Debug("servicenode balance", "balance", balance) - if balance.Cmp(big.NewInt(0)) != 0 { + if balance != 0 { t.Fatal("Expected balance to be 0 but it is not") } @@ -524,19 +410,17 @@ func TestSwapRPC(t *testing.T) { fake1 := int64(234) fake2 := int64(-100) - fakeBalance1 := big.NewInt(fake1) - fakeBalance2 := big.NewInt(fake2) - swap.peers[id1] = fakeBalance1 - swap.peers[id2] = fakeBalance2 + swap.balances[id1] = fake1 + swap.balances[id2] = fake2 err = rpcclient.Call(&balance, "swap_balanceWithPeer", id1) if err != nil { t.Fatal("servicenode RPC failed", "err", err) } log.Debug("balance1", "balance-1", balance) - if balance.Cmp(fakeBalance1) != 0 { - t.Fatal(fmt.Sprintf("Expected balance %s to be equal to fake balance %s, but it is not", balance.String(), fakeBalance1.String())) + if balance != fake1 { + t.Fatal(fmt.Sprintf("Expected balance %d to be equal to fake balance %d, but it is not", balance, fake1)) } err = rpcclient.Call(&balance, "swap_balanceWithPeer", id2) @@ -544,8 +428,8 @@ func TestSwapRPC(t *testing.T) { t.Fatal("servicenode RPC failed", "err", err) } log.Debug("balance2", "balance-2", balance) - if balance.Cmp(fakeBalance2) != 0 { - t.Fatal(fmt.Sprintf("Expected balance %s to be equal to fake balance %s, but it is not", balance.String(), fakeBalance2.String())) + if balance != fake2 { + t.Fatal(fmt.Sprintf("Expected balance %d to be equal to fake balance %d, but it is not", balance, fake2)) } err = rpcclient.Call(&balance, "swap_balance") @@ -554,9 +438,9 @@ func TestSwapRPC(t *testing.T) { } log.Debug("balance", "balance", balance) - fakeSum := big.NewInt(fake1 + fake2) - if balance.Cmp(fakeSum) != 0 { - t.Fatal(fmt.Sprintf("Expected balance %s to be equal to sum %s, but it is not", balance.String(), fakeSum.String())) + fakeSum := fake1 + fake2 + if balance != fakeSum { + t.Fatal(fmt.Sprintf("Expected balance %d to be equal to sum %d, but it is not", balance, fakeSum)) } } diff --git a/swarm/swap_test.go b/swarm/swap_test.go index fb0c5e4a35..48d3f601bf 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -18,9 +18,10 @@ package swarm import ( "context" + "errors" "fmt" "io/ioutil" - "math/big" + "math" "math/rand" "os" "strconv" @@ -103,13 +104,24 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { //run the simulation result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { + //wait for kademlia to be healthy + if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { + return err + } + nodeIDs := sim.UpNodeIDs() shuffle(len(nodeIDs), func(i, j int) { nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] }) //upload a file for every node for _, id := range nodeIDs { - key, data, err := uploadFile(sim.Service("swarm", id).(*Swarm)) + item, ok := sim.NodeItem(id, bucketKeySwarm) + if !ok { + log.Error("No swarm") + return errors.New("No swarm") + } + swarm := item.(*Swarm) + key, data, err := uploadFile(swarm) if err != nil { return err } @@ -121,89 +133,89 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { }) } - //wait for kademlia to be healthy - if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { - return err - } - // File retrieval check is repeated until all uploaded files are retrieved from all nodes // or until the timeout is reached. for { if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 { - return nil + break } } - }) + //every node has a map to all nodes it had interactions + //each entry in the map is a map of the other node with all the balances + balancesMap := make(map[enode.ID]map[enode.ID]int64) - //every node has a map to all nodes it had interactions - //each entry in the map is a map of the other node with all the balances - balancesMap := make(map[enode.ID]map[enode.ID]*big.Int) - - //iterate all nodes - for _, node := range sim.NodeIDs() { - item, ok := sim.NodeItem(node, bucketKeySwarm) - if !ok { - log.Error("No swarm") - return - } - swarm := item.(*Swarm) - - //submap for each node is a map of all nodes with the balance for that node - subBalances := make(map[enode.ID]*big.Int) - - //iterate all nodes again... - //get all balances with other peers for every node - for _, n := range sim.NodeIDs() { - if node == n { - continue + //iterate all nodes + for _, node := range sim.NodeIDs() { + item, ok := sim.NodeItem(node, bucketKeySwarm) + if !ok { + log.Error("No swarm") + return errors.New("No swarm") } + swarm := item.(*Swarm) - //get the peer's balance with this node - balance := swarm.swap.GetPeerBalance(n) - if balance != nil { - subBalances[n] = balance - log.Debug(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) - } else { - log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + //submap for each node is a map of all nodes with the balance for that node + subBalances := make(map[enode.ID]int64) + + //iterate all nodes again... + //get all balances with other peers for every node + for _, n := range sim.NodeIDs() { + if node == n { + continue + } + + //get the peer's balance with this node + balance, err := swarm.swap.GetPeerBalance(n) + if err == nil { + subBalances[n] = balance + log.Debug(fmt.Sprintf("Balance of node %s to node %s: %d", node.TerminalString(), n.TerminalString(), balance)) + } else { + log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + } + } + //update the map for this node + balancesMap[node] = subBalances + } + + //print all the balances if requested + if *printStats { + for k, v := range balancesMap { + fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) + for kk, vv := range v { + fmt.Println(fmt.Sprintf(".........with node %s: balance %d", kk.TerminalString(), vv)) + } } } - //update the map for this node - balancesMap[node] = subBalances - } - //print all the balances if requested - if *printStats { - for k, v := range balancesMap { - fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) - for kk, vv := range v { - fmt.Println(fmt.Sprintf(".........with node %s: balance %s", kk.TerminalString(), vv.String())) - } - } - } + //now iterate the whole map + //and check that every node k has the same + //balance with a peer as that peer with the node, + //but in inverted signs - //now iterate the whole map - //and check that every node k has the same - //balance with a peer as that peer with the node, - //but in inverted signs - - //iterate the map - for k, mapForK := range balancesMap { - //iterate the submap - for n, balanceKwithN := range mapForK { - //iterate the main map again - for subK, mapForSubK := range balancesMap { - //if the node and the peer are the same... - if n == subK { - log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) - log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) - //...check that they have the same balance in Abs terms and that it is not 0 - if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { - log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + //iterate the map + success := true + for k, mapForK := range balancesMap { + //iterate the submap + for n, balanceKwithN := range mapForK { + //iterate the main map again + for subK, mapForSubK := range balancesMap { + //if the node and the peer are the same... + if n == subK { + log.Trace(fmt.Sprintf("balance of %s with %s: %d", k.TerminalString(), n.TerminalString(), balanceKwithN)) + log.Trace(fmt.Sprintf("balance of %s with %s: %d", n.TerminalString(), k.TerminalString(), mapForSubK[k])) + //...check that they have the same balance in Abs terms and that it is not 0 + if math.Abs(float64(balanceKwithN)) != math.Abs(float64(mapForSubK[k])) && balanceKwithN != 0 { + log.Error(fmt.Sprintf("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not: %d, %d", balanceKwithN, mapForSubK[k])) + success = false + } } } } } - } + if success { + return nil + } + return errors.New("some conditions could not be met") + }) if result.Error != nil { t.Fatal(result.Error) @@ -309,60 +321,62 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { return nil } } - }) - balancesMap := make(map[enode.ID]map[enode.ID]*big.Int) + balancesMap := make(map[enode.ID]map[enode.ID]int64) - for _, node := range sim.NodeIDs() { - item, ok := sim.NodeItem(node, bucketKeySwarm) - if !ok { - log.Error("No swarm") - return - } - swarm := item.(*Swarm) - - subBalances := make(map[enode.ID]*big.Int) - - for _, n := range sim.NodeIDs() { - if node == n { - continue + for _, node := range sim.NodeIDs() { + item, ok := sim.NodeItem(node, bucketKeySwarm) + if !ok { + log.Error("No swarm") + return errors.New("no swarm") } - balance := swarm.swap.GetPeerBalance(n) - if balance != nil { - subBalances[n] = balance - log.Debug(fmt.Sprintf("Balance of node %s to node %s: %s", node.TerminalString(), n.TerminalString(), swarm.swap.GetPeerBalance(n).String())) - } else { - log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + swarm := item.(*Swarm) + + subBalances := make(map[enode.ID]int64) + + for _, n := range sim.NodeIDs() { + if node == n { + continue + } + balance, err := swarm.swap.GetPeerBalance(n) + if err == nil { + subBalances[n] = balance + log.Debug(fmt.Sprintf("Balance of node %s to node %s: %d", node.TerminalString(), n.TerminalString(), balance)) + } else { + log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) + } + } + balancesMap[node] = subBalances + } + + if *printStats { + for k, v := range balancesMap { + fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) + for kk, vv := range v { + fmt.Println(fmt.Sprintf(".........with node %s: balance %d", kk.TerminalString(), vv)) + } } } - balancesMap[node] = subBalances - } - if *printStats { - for k, v := range balancesMap { - fmt.Println(fmt.Sprintf("node %s balances:", k.TerminalString())) - for kk, vv := range v { - fmt.Println(fmt.Sprintf(".........with node %s: balance %s", kk.TerminalString(), vv.String())) - } - } - } - - /* - Assuming that in this case, balances should be symmetric too I - */ - for k, mapForK := range balancesMap { - for n, balanceKwithN := range mapForK { - for subK, mapForSubK := range balancesMap { - if n == subK { - log.Trace(fmt.Sprintf("balance of %s with %s: %s", k.TerminalString(), n.TerminalString(), balanceKwithN)) - log.Trace(fmt.Sprintf("balance of %s with %s: %s", n.TerminalString(), k.TerminalString(), mapForSubK[k])) - if balanceKwithN.CmpAbs(mapForSubK[k]) != 0 && balanceKwithN.Cmp(big.NewInt(0)) != 0 { - log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + /* + Assuming that in this case, balances should be symmetric too I + */ + for k, mapForK := range balancesMap { + for n, balanceKwithN := range mapForK { + for subK, mapForSubK := range balancesMap { + if n == subK { + log.Trace(fmt.Sprintf("balance of %s with %s: %d", k.TerminalString(), n.TerminalString(), balanceKwithN)) + log.Trace(fmt.Sprintf("balance of %s with %s: %d", n.TerminalString(), k.TerminalString(), mapForSubK[k])) + if math.Abs(float64(balanceKwithN)) != math.Abs(float64(mapForSubK[k])) && balanceKwithN != 0 { + log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + } } } } } - } + + return nil + }) if result.Error != nil { t.Fatal(result.Error) diff --git a/swarm/swarm.go b/swarm/swarm.go index fd6cbe242c..6fd4cb2bd1 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -174,6 +174,10 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e delivery := stream.NewDelivery(to, self.netStore) self.netStore.NewNetFetcherFunc = network.NewFetcherFactory(delivery.RequestFromPeers, config.DeliverySkipCheck).New + if config.SwapEnabled { + self.swap = swap.New(stateStore) + } + var nodeID enode.ID if err := nodeID.UnmarshalText([]byte(config.NodeID)); err != nil { return nil, err @@ -184,7 +188,7 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e DoRetrieve: true, SyncUpdateDelay: config.SyncUpdateDelay, MaxPeerServers: config.MaxStreamPeerServers, - }) + }, self.swap) // Swarm Hash Merklised Chunking for Arbitrary-length Document/File storage self.fileStore = storage.NewFileStore(self.netStore, self.config.FileStoreParams) @@ -207,11 +211,7 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e log.Debug("Setup local storage") - self.bzz = network.NewBzz(bzzconfig, to, stateStore, stream.Spec, self.streamer.Run) - - if config.SwapEnabled { - self.swap = swap.New(stateStore) - } + self.bzz = network.NewBzz(bzzconfig, to, stateStore, self.streamer.GetSpec(), self.streamer.Run) // Pss = postal service over swarm (devp2p over bzz) self.ps, err = pss.NewPss(to, config.Pss) From 9479baed51420a01124a2330a1c51ea2c161a8db Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Sat, 6 Oct 2018 10:33:52 -0500 Subject: [PATCH 21/30] swarm/swap: make streamer spec work with p2p accounting --- swarm/network/stream/stream.go | 39 +++++++++++++++++++++------------- swarm/swap_test.go | 1 + 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index 12ff7120e5..853d7f1932 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -63,6 +63,7 @@ type Registry struct { maxPeerServers int balanceMgr protocols.BalanceManager priceOracle protocols.PriceOracle + spec *protocols.Spec } // RegistryOptions holds optional values for NewRegistry constructor. @@ -82,6 +83,7 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy if options.SyncUpdateDelay <= 0 { options.SyncUpdateDelay = 15 * time.Second } + streamer := &Registry{ addr: localID, skipCheck: options.SkipCheck, @@ -92,7 +94,10 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy intervalsStore: intervalsStore, doRetrieve: options.DoRetrieve, maxPeerServers: options.MaxPeerServers, + balanceMgr: balanceMgr, } + streamer.setupSpec() + streamer.api = NewAPI(streamer) delivery.getPeer = streamer.getPeer streamer.RegisterServerFunc(swarmChunkServerStreamName, func(_ *Peer, _ string, _ bool) (Server, error) { @@ -182,12 +187,15 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy }() } - streamer.balanceMgr = balanceMgr - streamer.priceOracle = streamer.createPriceOracle() - return streamer } +func (r *Registry) setupSpec() { + r.createSpec() + r.createPriceOracle() + r.spec.Hook = protocols.NewAccountingHook(r.balanceMgr, r.priceOracle) +} + // RegisterClient registers an incoming streamer constructor func (r *Registry) RegisterClientFunc(stream string, f func(*Peer, string, bool) (Client, error)) { r.clientMu.Lock() @@ -453,7 +461,7 @@ func (r *Registry) updateSyncing() { } func (r *Registry) runProtocol(p *p2p.Peer, rw p2p.MsgReadWriter) error { - peer := protocols.NewPeer(p, rw, r.GetSpec()) + peer := protocols.NewPeer(p, rw, r.spec) bp := network.NewBzzPeer(peer) np := network.NewPeer(bp, r.delivery.kad) r.delivery.kad.On(np) @@ -678,6 +686,10 @@ func (c *clientParams) clientCreated() { } func (r *Registry) GetSpec() *protocols.Spec { + return r.spec +} + +func (r *Registry) createSpec() { // Spec is the spec of the streamer protocol var spec = &protocols.Spec{ @@ -697,9 +709,8 @@ func (r *Registry) GetSpec() *protocols.Spec { QuitMsg{}, ChunkDeliveryMsgSyncing{}, }, - Hook: protocols.NewAccountingHook(r.balanceMgr, r.priceOracle), } - return spec + r.spec = spec } //An accountable message needs some meta information attached to it @@ -715,7 +726,7 @@ type StreamerPriceOracle struct { } func (spo *StreamerPriceOracle) Accountable(msg interface{}) bool { - code, ok := spo.registry.GetSpec().GetCode(msg) + code, ok := spo.registry.spec.GetCode(msg) if !ok { return false } @@ -726,7 +737,7 @@ func (spo *StreamerPriceOracle) Accountable(msg interface{}) bool { } func (spo *StreamerPriceOracle) Price(size uint32, msg interface{}) (direction protocols.EntryDirection, price uint64) { - code, ok := spo.registry.GetSpec().GetCode(msg) + code, ok := spo.registry.spec.GetCode(msg) if !ok { panic("Attempting to get message code for an expected message type, but code not found") } @@ -740,14 +751,14 @@ func (spo *StreamerPriceOracle) Price(size uint32, msg interface{}) (direction p return } -func (r *Registry) createPriceOracle() protocols.PriceOracle { +func (r *Registry) createPriceOracle() { po := &StreamerPriceOracle{ registry: r, } po.priceMatrix = make(map[uint64]*priceTag) - deliveryCode, ok := r.GetSpec().GetCode(ChunkDeliveryMsgRetrieval{}) + deliveryCode, ok := r.spec.GetCode(ChunkDeliveryMsgRetrieval{}) if !ok { panic("Attempting to get message code for an expected message type, but code not found") } @@ -758,7 +769,7 @@ func (r *Registry) createPriceOracle() protocols.PriceOracle { } po.priceMatrix[deliveryCode] = tag - retrieveReqCode, ok := r.GetSpec().GetCode(RetrieveRequestMsg{}) + retrieveReqCode, ok := r.spec.GetCode(RetrieveRequestMsg{}) if !ok { panic("Attempting to get message code for an expected message type, but code not found") } @@ -768,13 +779,11 @@ func (r *Registry) createPriceOracle() protocols.PriceOracle { direction: protocols.ChargeSender, } po.priceMatrix[retrieveReqCode] = tag - - return po - + r.priceOracle = po } func (r *Registry) Protocols() []p2p.Protocol { - spec := r.GetSpec() + spec := r.spec return []p2p.Protocol{ { Name: spec.Name, diff --git a/swarm/swap_test.go b/swarm/swap_test.go index 48d3f601bf..85ae81b977 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -231,6 +231,7 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { sim := simulation.New(map[string]simulation.ServiceFunc{ "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { config := api.NewConfig() + config.Port = strconv.Itoa(8500 + rand.Intn(9999)) dir, err := ioutil.TempDir("", "swap-network-test-node") if err != nil { From 8e186d8a46eafa163a9a7ab06feb934ea9fd0ae8 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Sat, 6 Oct 2018 10:45:44 -0500 Subject: [PATCH 22/30] swarm/swap: rebased to latest master --- swarm/swap_test.go | 21 ++++++++++++++------- swarm/swarm.go | 1 - 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/swarm/swap_test.go b/swarm/swap_test.go index 85ae81b977..882d726f19 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -54,7 +54,7 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { config := api.NewConfig() config.Port = strconv.Itoa(8500 + rand.Intn(9999)) - dir, err := ioutil.TempDir("", "swap-network-test-node") + dir, err := ioutil.TempDir("", "swap-network-test-node"+config.Port) if err != nil { return nil, nil, err } @@ -233,7 +233,7 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { config := api.NewConfig() config.Port = strconv.Itoa(8500 + rand.Intn(9999)) - dir, err := ioutil.TempDir("", "swap-network-test-node") + dir, err := ioutil.TempDir("", "swap-network-test-node"+config.Port) if err != nil { return nil, nil, err } @@ -290,6 +290,10 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { pseudoRandomBitMask := strconv.FormatInt(pseudoRandomNum, 2) result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { + if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { + return err + } + nodeIDs := sim.UpNodeIDs() shuffle(len(nodeIDs), func(i, j int) { nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] @@ -311,10 +315,6 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { } } - if _, err := sim.WaitTillHealthy(ctx, 2); err != nil { - return err - } - // File retrieval check is repeated until all uploaded files are retrieved from all nodes // or until the timeout is reached. for { @@ -362,6 +362,8 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { /* Assuming that in this case, balances should be symmetric too I */ + + success := true for k, mapForK := range balancesMap { for n, balanceKwithN := range mapForK { for subK, mapForSubK := range balancesMap { @@ -370,13 +372,18 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { log.Trace(fmt.Sprintf("balance of %s with %s: %d", n.TerminalString(), k.TerminalString(), mapForSubK[k])) if math.Abs(float64(balanceKwithN)) != math.Abs(float64(mapForSubK[k])) && balanceKwithN != 0 { log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + success = false } } } } } - return nil + if success { + return nil + } + + return errors.New("some conditions could not be met") }) if result.Error != nil { diff --git a/swarm/swarm.go b/swarm/swarm.go index 6fd4cb2bd1..8c0a327fad 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -51,7 +51,6 @@ import ( "github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/swarm/storage/feed" "github.com/ethereum/go-ethereum/swarm/storage/mock" - "github.com/ethereum/go-ethereum/swarm/storage/mru" "github.com/ethereum/go-ethereum/swarm/swap" "github.com/ethereum/go-ethereum/swarm/tracing" ) From b1c51e7505b507ca406edc835e72345d28edefbe Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Sun, 7 Oct 2018 19:58:33 -0500 Subject: [PATCH 23/30] swarm/swap: locks and bugfixing --- p2p/protocols/accounting.go | 9 +++++++++ swarm/network/stream/stream.go | 5 ++++- swarm/swap_test.go | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/p2p/protocols/accounting.go b/p2p/protocols/accounting.go index 4c3e95e67b..a81fe5c318 100644 --- a/p2p/protocols/accounting.go +++ b/p2p/protocols/accounting.go @@ -16,6 +16,10 @@ package protocols +import ( + "sync" +) + type PriceOracle interface { Price(uint32, interface{}) (EntryDirection, uint64) Accountable(interface{}) bool @@ -38,6 +42,7 @@ const ( type AccountingHook struct { BalanceManager PriceOracle + lock sync.RWMutex //lock the balances } func NewAccountingHook(mgr BalanceManager, po PriceOracle) *AccountingHook { @@ -49,6 +54,8 @@ func NewAccountingHook(mgr BalanceManager, po PriceOracle) *AccountingHook { } func (ah *AccountingHook) Send(peer *Peer, size uint32, msg interface{}) error { + ah.lock.Lock() + defer ah.lock.Unlock() var err error if !ah.PriceOracle.Accountable(msg) { return nil @@ -63,6 +70,8 @@ func (ah *AccountingHook) Send(peer *Peer, size uint32, msg interface{}) error { } func (ah *AccountingHook) Receive(peer *Peer, size uint32, msg interface{}) error { + ah.lock.Lock() + defer ah.lock.Unlock() var err error if !ah.PriceOracle.Accountable(msg) { return nil diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index 853d7f1932..ede6cea3a9 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "math" + "reflect" "sync" "time" @@ -193,7 +194,9 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy func (r *Registry) setupSpec() { r.createSpec() r.createPriceOracle() - r.spec.Hook = protocols.NewAccountingHook(r.balanceMgr, r.priceOracle) + if !reflect.ValueOf(r.balanceMgr).IsNil() { + r.spec.Hook = protocols.NewAccountingHook(r.balanceMgr, r.priceOracle) + } } // RegisterClient registers an incoming streamer constructor diff --git a/swarm/swap_test.go b/swarm/swap_test.go index 882d726f19..755a698c1c 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -319,7 +319,7 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { // or until the timeout is reached. for { if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 { - return nil + break } } From fe7ffe3eb12299a0d7488b3304391047b6789787 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Mon, 8 Oct 2018 13:26:30 -0500 Subject: [PATCH 24/30] swarm/swap: introduced sleep after retrieval for network test --- p2p/protocols/protocol.go | 3 +++ swarm/swap/swap.go | 5 ++--- swarm/swap/swap_test.go | 47 +++++++++++++++++++++++++++++++++++++++ swarm/swap_test.go | 22 ++++++++++-------- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/p2p/protocols/protocol.go b/p2p/protocols/protocol.go index b0e1c3a865..5cbc254bca 100644 --- a/p2p/protocols/protocol.go +++ b/p2p/protocols/protocol.go @@ -353,6 +353,9 @@ func (p *Peer) handleIncoming(handle func(ctx context.Context, msg interface{}) } if p.spec.Hook != nil { + if wmsg.Size != uint32(len(wmsg.Payload)) { + log.Warn("Advertised message size and payload length don't match") + } err := p.spec.Hook.Receive(p, wmsg.Size, val) if err != nil { return err diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index fd5a334fb6..5e96514b0c 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -32,14 +32,13 @@ import ( const ( defaultMaxMsgSize = 1024 * 1024 - OracleID = "swap" swapProtocolName = "swap" swapVersion = 1 ) var ( - payAt = int64(-4096 * 10000) // threshold that triggers payment {request} (bytes) - dropAt = int64(-4096 * 12000) // threshold that triggers disconnect (bytes) + payAt = int64(-4096 * 10000000) // threshold that triggers payment {request} (bytes) + dropAt = int64(-4096 * 12000000) // threshold that triggers disconnect (bytes) ErrNotAccountedMsg = errors.New("Message does not need accounting") ErrInsufficientFunds = errors.New("Insufficient funds") diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index f2b460d92d..297fbf798d 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -23,6 +23,7 @@ import ( "fmt" "io/ioutil" "math" + mrand "math/rand" "os" "path/filepath" "testing" @@ -148,6 +149,52 @@ func TestLimits(t *testing.T) { } } +//check that the disconnect threshold is below the payment threshold +func TestRepeatedBookings(t *testing.T) { + //create a test swap account + swap, testDir := createTestSwap(t) + defer os.RemoveAll(testDir) + + testPeer := newDummyPeer() + amount := mrand.Intn(100) + cnt := 1 + mrand.Intn(10) + for i := 0; i < cnt; i++ { + swap.Credit(testPeer.Peer.Peer, uint64(amount)) + } + expectedBalance := int64(cnt * amount) + realBalance := swap.balances[testPeer.ID()] + if expectedBalance != realBalance { + t.Fatal(fmt.Sprintf("After %d credits of %d, expected balance to be: %d, but is: %d", cnt, amount, expectedBalance, realBalance)) + } + + testPeer2 := newDummyPeer() + amount = mrand.Intn(100) + cnt = 1 + mrand.Intn(10) + for i := 0; i < cnt; i++ { + swap.Debit(testPeer2.Peer.Peer, uint64(amount)) + } + expectedBalance = int64(0 - (cnt * amount)) + realBalance = swap.balances[testPeer2.ID()] + if expectedBalance != realBalance { + t.Fatal(fmt.Sprintf("After %d debits of %d, expected balance to be: %d, but is: %d", cnt, amount, expectedBalance, realBalance)) + } + + //mixed debits and credits + amount1 := mrand.Intn(100) + amount2 := mrand.Intn(100) + amount3 := mrand.Intn(100) + swap.Credit(testPeer2.Peer.Peer, uint64(amount1)) + swap.Credit(testPeer2.Peer.Peer, uint64(amount2)) + swap.Debit(testPeer2.Peer.Peer, uint64(amount3)) + + expectedBalance = expectedBalance + int64(amount1+amount2-amount3) + realBalance = swap.balances[testPeer2.ID()] + + if expectedBalance != realBalance { + t.Fatal(fmt.Sprintf("After mixed debits and credits, expected balance to be: %d, but is: %d", expectedBalance, realBalance)) + } +} + //unit test for exceeds pay limit //when the payment threshold is reached, a cheque will be issued //this test checks that a cheque is present if a message is sent diff --git a/swarm/swap_test.go b/swarm/swap_test.go index 755a698c1c..1a34626dcc 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "io/ioutil" - "math" "math/rand" "os" "strconv" @@ -140,6 +139,8 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { break } } + + time.Sleep(5 * time.Second) //every node has a map to all nodes it had interactions //each entry in the map is a map of the other node with all the balances balancesMap := make(map[enode.ID]map[enode.ID]int64) @@ -165,6 +166,7 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { //get the peer's balance with this node balance, err := swarm.swap.GetPeerBalance(n) + fmt.Println(balance) if err == nil { subBalances[n] = balance log.Debug(fmt.Sprintf("Balance of node %s to node %s: %d", node.TerminalString(), n.TerminalString(), balance)) @@ -203,8 +205,8 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { log.Trace(fmt.Sprintf("balance of %s with %s: %d", k.TerminalString(), n.TerminalString(), balanceKwithN)) log.Trace(fmt.Sprintf("balance of %s with %s: %d", n.TerminalString(), k.TerminalString(), mapForSubK[k])) //...check that they have the same balance in Abs terms and that it is not 0 - if math.Abs(float64(balanceKwithN)) != math.Abs(float64(mapForSubK[k])) && balanceKwithN != 0 { - log.Error(fmt.Sprintf("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not: %d, %d", balanceKwithN, mapForSubK[k])) + if balanceKwithN+mapForSubK[k] != 0 && balanceKwithN != 0 { + log.Error(fmt.Sprintf("Expected balances to be a+b = 0 AND balance(a) != 0, but they are not, balance k with n: %d, balance n with k: %d", balanceKwithN, mapForSubK[k])) success = false } } @@ -278,10 +280,10 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { t.Fatal(err) } - //this is actually quite a big maxFileSize, which results - //in the test running for nearly 2 minutes - //maybe for the test, we could reduce it - const maxFileSize = 1024 * 1024 * 4 //1024 bytes * 1024 * 4 = 4MB + //NOTE: maxFileSize is 4 kB, this in order to provide faster tests + //it would be interesting to run these tests with bigger files + //(to see how drop limits are affected etc.) + const maxFileSize = 1024 * 4 //1024 bytes * 4 = 4kB const minfileSize = 1024 //pseudo random algo to define if a node will upload or not @@ -323,6 +325,8 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { } } + time.Sleep(5 * time.Second) + balancesMap := make(map[enode.ID]map[enode.ID]int64) for _, node := range sim.NodeIDs() { @@ -370,8 +374,8 @@ func TestSwapNetworkAsymmetricFileUpload(t *testing.T) { if n == subK { log.Trace(fmt.Sprintf("balance of %s with %s: %d", k.TerminalString(), n.TerminalString(), balanceKwithN)) log.Trace(fmt.Sprintf("balance of %s with %s: %d", n.TerminalString(), k.TerminalString(), mapForSubK[k])) - if math.Abs(float64(balanceKwithN)) != math.Abs(float64(mapForSubK[k])) && balanceKwithN != 0 { - log.Error("Expected balances to be |abs| = 0 AND balance1 != 0, but they are not") + if balanceKwithN+mapForSubK[k] != 0 && balanceKwithN != 0 { + log.Error(fmt.Sprintf("Expected balances to be a+b = 0 AND balance(a) != 0, but they are not, balance k with n: %d, balance n with k: %d", balanceKwithN, mapForSubK[k])) success = false } } From bef192d5faaf777468ed98b5fb0a3982dc16de12 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Mon, 8 Oct 2018 17:25:11 -0500 Subject: [PATCH 25/30] swarm/swap: first accounting metrics --- p2p/protocols/accounting.go | 12 ++--- swarm/swap/api.go | 48 +++++++++++++++-- swarm/swap/swap.go | 102 ++++++++++++++++++++++++++++-------- swarm/swap/swap_test.go | 12 +++-- swarm/swap_test.go | 18 ++++++- 5 files changed, 154 insertions(+), 38 deletions(-) diff --git a/p2p/protocols/accounting.go b/p2p/protocols/accounting.go index a81fe5c318..3395588d62 100644 --- a/p2p/protocols/accounting.go +++ b/p2p/protocols/accounting.go @@ -27,9 +27,9 @@ type PriceOracle interface { type BalanceManager interface { //Credit is crediting the peer, charging local node - Credit(peer *Peer, amount uint64) error + Credit(peer *Peer, amount uint64, size uint32) error //Debit is crediting the local node, charging the remote peer - Debit(peer *Peer, amount uint64) error + Debit(peer *Peer, amount uint64, size uint32) error } type EntryDirection bool @@ -62,9 +62,9 @@ func (ah *AccountingHook) Send(peer *Peer, size uint32, msg interface{}) error { } direction, price := ah.PriceOracle.Price(size, msg) if direction == ChargeSender { - err = ah.BalanceManager.Debit(peer, price) + err = ah.BalanceManager.Debit(peer, price, size) } else { - err = ah.BalanceManager.Credit(peer, price) + err = ah.BalanceManager.Credit(peer, price, size) } return err } @@ -78,9 +78,9 @@ func (ah *AccountingHook) Receive(peer *Peer, size uint32, msg interface{}) erro } direction, price := ah.PriceOracle.Price(size, msg) if direction == ChargeReceiver { - err = ah.BalanceManager.Debit(peer, price) + err = ah.BalanceManager.Debit(peer, price, size) } else { - err = ah.BalanceManager.Credit(peer, price) + err = ah.BalanceManager.Credit(peer, price, size) } return err } diff --git a/swarm/swap/api.go b/swarm/swap/api.go index efdd94f092..70aecb876c 100644 --- a/swarm/swap/api.go +++ b/swarm/swap/api.go @@ -32,9 +32,21 @@ type API struct { swap *Swap } -//TODO: define metrics //Get metrics about swap for this node -type SwapMetrics struct { +//The current metrics are for accounted message types only +//(i.e. BytesTransferred is amount of bytes sent but only for a +//accounted message types) +type Metrics struct { + BalanceCredited uint64 + BalanceDebited uint64 + BytesCredited uint64 + BytesDebited uint64 + MsgCredited uint64 + MsgDebited uint64 + ChequesIssued uint64 + ChequesReceived uint64 + PeerDrops uint64 + SelfDrops uint64 } //Create a new API instance @@ -55,7 +67,7 @@ func (swapapi *API) BalanceWithPeer(ctx context.Context, peer enode.ID) (balance //Get the overall balance of the node //Iterates over all peers this node is having accounted interaction //and just adds up balances. -//It assumes that if a disfavorable balance is represented as a negative value +//It assumes that a disfavorable balance is represented as a negative value func (swapapi *API) Balance(ctx context.Context) (balance int64, err error) { balance = 0 for _, peerBalance := range swapapi.swap.balances { @@ -65,6 +77,32 @@ func (swapapi *API) Balance(ctx context.Context) (balance int64, err error) { } //Just return the Swap metrics -func (swapapi *API) GetSwapMetrics() (*SwapMetrics, error) { - return nil, nil +func (swapapi *API) GetSwapMetricsForPeer(ctx context.Context, peer enode.ID) (*Metrics, error) { + var ok bool + var metrics *Metrics + metrics, ok = swapapi.swap.metrics[peer] + if !ok { + return nil, ErrNoSuchPeerAccounting + } + return metrics, nil +} + +//Just return the Swap metrics +func (swapapi *API) GetSwapMetrics(ctx context.Context, peer enode.ID) (*Metrics, error) { + var metrics *Metrics + + for _, m := range swapapi.swap.metrics { + metrics.BalanceCredited += m.BalanceCredited + metrics.BalanceDebited += m.BalanceDebited + metrics.BytesCredited += m.BytesCredited + metrics.BytesDebited += m.BytesDebited + metrics.ChequesIssued += m.ChequesIssued + metrics.ChequesReceived += m.ChequesReceived + metrics.MsgCredited += m.MsgCredited + metrics.MsgDebited += m.MsgDebited + metrics.PeerDrops += m.PeerDrops + metrics.SelfDrops += m.SelfDrops + } + + return metrics, nil } diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 5e96514b0c..65a9e63c51 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -40,7 +40,6 @@ var ( payAt = int64(-4096 * 10000000) // threshold that triggers payment {request} (bytes) dropAt = int64(-4096 * 12000000) // threshold that triggers disconnect (bytes) - ErrNotAccountedMsg = errors.New("Message does not need accounting") ErrInsufficientFunds = errors.New("Insufficient funds") ) @@ -49,30 +48,28 @@ var ( // A node maintains an individual balance with every peer // Only messages which have a price will be accounted for type Swap struct { - chequeManager *ChequeManager //cheque manager keeps track of issued cheques - stateStore state.Store //stateStore is needed in order to keep balances across sessions - lock sync.RWMutex //lock the balances - balances map[enode.ID]int64 //map of balances for each peer - protocol *Protocol //reference to the cheque exchange protocol + chequeManager *ChequeManager //cheque manager keeps track of issued cheques + stateStore state.Store //stateStore is needed in order to keep balances across sessions + lock sync.RWMutex //lock the balances + balances map[enode.ID]int64 //map of balances for each peer + metrics map[enode.ID]*Metrics //map of metrics for each peer + protocol *Protocol //reference to the cheque exchange protocol } //Credit us and debit remote -func (s *Swap) Credit(peer *protocols.Peer, amount uint64) (err error) { +func (s *Swap) Credit(peer *protocols.Peer, amount uint64, size uint32) (err error) { s.lock.Lock() defer s.lock.Unlock() - peerBalance := s.balances[peer.ID()] - if _, ok := s.balances[peer.ID()]; !ok { - s.stateStore.Get(peer.ID().String(), &peerBalance) - s.balances[peer.ID()] = peerBalance - } - //local node is being credited(in favor of local node), so the balance increases + s.loadState(peer) + s.balances[peer.ID()] += int64(amount) - peerBalance = s.balances[peer.ID()] + peerBalance := s.balances[peer.ID()] s.stateStore.Put(peer.ID().String(), &peerBalance) if float64(peerBalance) > math.Abs(float64(payAt)) { ctx := context.TODO() + //WARNING: Should we do this? Otherwise anyone could create a WantChequeMsg requiring us to handle special situations... err := s.wantCheque(ctx, peer.ID()) if err != nil { //TODO: special error handling, as at this point the accounting has been done @@ -81,29 +78,32 @@ func (s *Swap) Credit(peer *protocols.Peer, amount uint64) (err error) { } } if float64(peerBalance) > math.Abs(float64(dropAt)) { + s.metrics[peer.ID()].PeerDrops += 1 return ErrInsufficientFunds } + //TODO: size for metrics is currently misleading: should only account for size based messages(?) + s.updatePeerMetrics(peer, true, amount, size) + log.Debug(fmt.Sprintf("balance for peer %s: %s", peer.ID().String(), strconv.FormatInt(peerBalance, 10))) return err } //Debit us and credit remote -func (s *Swap) Debit(peer *protocols.Peer, amount uint64) (err error) { +func (s *Swap) Debit(peer *protocols.Peer, amount uint64, size uint32) (err error) { s.lock.Lock() defer s.lock.Unlock() - peerBalance := s.balances[peer.ID()] - if _, ok := s.balances[peer.ID()]; !ok { - s.stateStore.Get(peer.ID().String(), &peerBalance) - s.balances[peer.ID()] = peerBalance - } + s.loadState(peer) + //local node is being debited (in favor of remote peer), so its balance decreases s.balances[peer.ID()] -= int64(amount) - peerBalance = s.balances[peer.ID()] + peerBalance := s.balances[peer.ID()] s.stateStore.Put(peer.ID().String(), &peerBalance) + if peerBalance < payAt { ctx := context.TODO() err := s.issueCheque(ctx, peer.ID()) + s.metrics[peer.ID()].ChequesIssued += 1 if err != nil { //TODO: special error handling, as at this point the accounting has been done //but the cheque could not be sent? @@ -111,8 +111,12 @@ func (s *Swap) Debit(peer *protocols.Peer, amount uint64) (err error) { } } if peerBalance < dropAt { + s.metrics[peer.ID()].SelfDrops += 1 return ErrInsufficientFunds } + + s.updatePeerMetrics(peer, false, amount, size) + log.Debug(fmt.Sprintf("balance for peer %s: %s", peer.ID().String(), strconv.FormatInt(peerBalance, 10))) return nil } @@ -127,6 +131,61 @@ func (swap *Swap) GetPeerBalance(peer enode.ID) (int64, error) { return 0, errors.New("Peer not found") } +func (swap *Swap) GetPeerMetrics(peer enode.ID) (*Metrics, error) { + swap.lock.RLock() + defer swap.lock.RUnlock() + if p, ok := swap.metrics[peer]; ok { + return p, nil + } + return nil, errors.New("Peer not found") +} + +func (s *Swap) loadState(peer *protocols.Peer) { + var peerBalance int64 + var peerMetrics *Metrics + peerID := peer.ID() + if _, ok := s.metrics[peerID]; !ok { + s.stateStore.Get("metrics"+peerID.String(), &peerMetrics) + if peerMetrics == nil { + peerMetrics = &Metrics{ + BalanceCredited: 0, + BalanceDebited: 0, + BytesCredited: 0, + BytesDebited: 0, + MsgCredited: 0, + MsgDebited: 0, + ChequesIssued: 0, + ChequesReceived: 0, + PeerDrops: 0, + SelfDrops: 0, + } + s.metrics[peerID] = peerMetrics + } + } + if _, ok := s.balances[peerID]; !ok { + s.stateStore.Get(peerID.String(), &peerBalance) + s.balances[peerID] = peerBalance + } +} + +//local node is being credited(in favor of local node), so the balance increases +func (s *Swap) updatePeerMetrics(peer *protocols.Peer, credit bool, amount uint64, size uint32) { + + metrics := s.metrics[peer.ID()] + + if credit { + metrics.BalanceCredited += amount + metrics.BytesCredited += uint64(size) + metrics.MsgCredited += 1 + } else { + metrics.BalanceDebited += amount + metrics.BytesDebited += uint64(size) + metrics.MsgDebited += 1 + } + + s.stateStore.Put("metrics"+peer.ID().String(), metrics) +} + //Issue a cheque for the remote peer. Happens if we are indebted with the peer //and crossed the payment threshold func (s *Swap) issueCheque(ctx context.Context, id enode.ID) error { @@ -158,6 +217,7 @@ func New(stateStore state.Store) (swap *Swap) { chequeManager: NewChequeManager(stateStore), stateStore: stateStore, balances: make(map[enode.ID]int64), + metrics: make(map[enode.ID]*Metrics), protocol: NewProtocol(), } return diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 297fbf798d..61e6955a83 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -156,10 +156,12 @@ func TestRepeatedBookings(t *testing.T) { defer os.RemoveAll(testDir) testPeer := newDummyPeer() + //size is irrelevant for this test + size := uint32(0) amount := mrand.Intn(100) cnt := 1 + mrand.Intn(10) for i := 0; i < cnt; i++ { - swap.Credit(testPeer.Peer.Peer, uint64(amount)) + swap.Credit(testPeer.Peer.Peer, uint64(amount), size) } expectedBalance := int64(cnt * amount) realBalance := swap.balances[testPeer.ID()] @@ -171,7 +173,7 @@ func TestRepeatedBookings(t *testing.T) { amount = mrand.Intn(100) cnt = 1 + mrand.Intn(10) for i := 0; i < cnt; i++ { - swap.Debit(testPeer2.Peer.Peer, uint64(amount)) + swap.Debit(testPeer2.Peer.Peer, uint64(amount), size) } expectedBalance = int64(0 - (cnt * amount)) realBalance = swap.balances[testPeer2.ID()] @@ -183,9 +185,9 @@ func TestRepeatedBookings(t *testing.T) { amount1 := mrand.Intn(100) amount2 := mrand.Intn(100) amount3 := mrand.Intn(100) - swap.Credit(testPeer2.Peer.Peer, uint64(amount1)) - swap.Credit(testPeer2.Peer.Peer, uint64(amount2)) - swap.Debit(testPeer2.Peer.Peer, uint64(amount3)) + swap.Credit(testPeer2.Peer.Peer, uint64(amount1), size) + swap.Credit(testPeer2.Peer.Peer, uint64(amount2), size) + swap.Debit(testPeer2.Peer.Peer, uint64(amount3), size) expectedBalance = expectedBalance + int64(amount1+amount2-amount3) realBalance = swap.balances[testPeer2.ID()] diff --git a/swarm/swap_test.go b/swarm/swap_test.go index 1a34626dcc..dc47ffd85e 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -166,13 +166,29 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { //get the peer's balance with this node balance, err := swarm.swap.GetPeerBalance(n) - fmt.Println(balance) if err == nil { subBalances[n] = balance log.Debug(fmt.Sprintf("Balance of node %s to node %s: %d", node.TerminalString(), n.TerminalString(), balance)) } else { log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) } + metrics, err := swarm.swap.GetPeerMetrics(n) + if err == nil && *printStats { + fmt.Println(fmt.Sprintf("********** Metrics for node %s with node %s: *************", node.TerminalString(), n.TerminalString())) + fmt.Println(fmt.Sprintf("Total units credited: %d", metrics.BalanceCredited)) + fmt.Println(fmt.Sprintf("Total units debited: %d", metrics.BalanceDebited)) + fmt.Println(fmt.Sprintf("Total bytes credited: %d", metrics.BytesCredited)) + fmt.Println(fmt.Sprintf("Total bytes debited: %d", metrics.BytesDebited)) + fmt.Println(fmt.Sprintf("Cheques issued: %d", metrics.ChequesIssued)) + fmt.Println(fmt.Sprintf("Cheques received: %d", metrics.ChequesReceived)) + fmt.Println(fmt.Sprintf("Number of messages credited: %d", metrics.MsgCredited)) + fmt.Println(fmt.Sprintf("Number of messages debited: %d", metrics.MsgDebited)) + fmt.Println(fmt.Sprintf("Peers dropped: %d", metrics.PeerDrops)) + fmt.Println(fmt.Sprintf("Number of times node dropped itself: %d", metrics.SelfDrops)) + } else { + //not all peers have metrics with every node, so probably can be ignored + log.Debug("Error getting metrics", "err", err) + } } //update the map for this node balancesMap[node] = subBalances From 4b6892c6270c689b80927bafa8e4070927a8467c Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Wed, 10 Oct 2018 12:42:14 -0500 Subject: [PATCH 26/30] swarm/swap: leaner first phase, bug fixes and PR comments --- metrics/counter.go | 34 ++++- p2p/protocols/accounting.go | 72 +++++++--- swarm/network/stream/stream.go | 20 +-- swarm/swap/api.go | 108 --------------- swarm/swap/chequemanager.go | 63 --------- swarm/swap/protocol.go | 203 ---------------------------- swarm/swap/swap.go | 139 +------------------- swarm/swap/swap_test.go | 233 ++------------------------------- swarm/swap_test.go | 29 ++-- swarm/swarm.go | 7 - 10 files changed, 122 insertions(+), 786 deletions(-) delete mode 100644 swarm/swap/api.go delete mode 100644 swarm/swap/chequemanager.go delete mode 100644 swarm/swap/protocol.go diff --git a/metrics/counter.go b/metrics/counter.go index c7f2b4bd3a..2f78c90d5c 100644 --- a/metrics/counter.go +++ b/metrics/counter.go @@ -1,6 +1,8 @@ package metrics -import "sync/atomic" +import ( + "sync/atomic" +) // Counters hold an int64 value that can be incremented and decremented. type Counter interface { @@ -20,6 +22,17 @@ func GetOrRegisterCounter(name string, r Registry) Counter { return r.GetOrRegister(name, NewCounter).(Counter) } +// GetOrRegisterCounterForced returns an existing Counter or constructs and registers a +// new Counter no matter the global switch is enabled or not. +// Be sure to unregister the counter from the registry once it is of no use to +// allow for garbage collection. +func GetOrRegisterCounterForced(name string, r Registry) Counter { + if nil == r { + r = DefaultRegistry + } + return r.GetOrRegister(name, NewCounterForced).(Counter) +} + // NewCounter constructs a new StandardCounter. func NewCounter() Counter { if !Enabled { @@ -28,6 +41,12 @@ func NewCounter() Counter { return &StandardCounter{0} } +// NewCounterForced constructs a new StandardCounter and returns it no matter if +// the global switch is enabled or not. +func NewCounterForced() Counter { + return &StandardCounter{0} +} + // NewRegisteredCounter constructs and registers a new StandardCounter. func NewRegisteredCounter(name string, r Registry) Counter { c := NewCounter() @@ -38,6 +57,19 @@ func NewRegisteredCounter(name string, r Registry) Counter { return c } +// NewRegisteredCounterForced constructs and registers a new StandardCounter +// and launches a goroutine no matter the global switch is enabled or not. +// Be sure to unregister the counter from the registry once it is of no use to +// allow for garbage collection. +func NewRegisteredCounterForced(name string, r Registry) Counter { + c := NewCounterForced() + if nil == r { + r = DefaultRegistry + } + r.Register(name, c) + return c +} + // CounterSnapshot is a read-only copy of another Counter. type CounterSnapshot int64 diff --git a/p2p/protocols/accounting.go b/p2p/protocols/accounting.go index 3395588d62..2319b6a99b 100644 --- a/p2p/protocols/accounting.go +++ b/p2p/protocols/accounting.go @@ -18,25 +18,41 @@ package protocols import ( "sync" + + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + //NOTE: these metrics just define the interfaces and are currently *NOT persisted* over sessions + mBalanceCredit = metrics.NewRegisteredCounterForced("account.balance.credit", nil) + mBalanceDebit = metrics.NewRegisteredCounterForced("account.balance.debit", nil) + mBytesCredit = metrics.NewRegisteredCounterForced("account.bytes.credit", nil) + mBytesDebit = metrics.NewRegisteredCounterForced("account.bytes.debit", nil) + mMsgCredit = metrics.NewRegisteredCounterForced("account.msg.credit", nil) + mMsgDebit = metrics.NewRegisteredCounterForced("account.msg.debit", nil) + mPeerDrops = metrics.NewRegisteredCounterForced("account.peerdrops", nil) + mSelfDrops = metrics.NewRegisteredCounterForced("account.selfdrops", nil) + mChequesIssued = metrics.NewRegisteredCounterForced("account.cheques.issued", nil) + mChequesReceived = metrics.NewRegisteredCounterForced("account.cheques.received", nil) ) type PriceOracle interface { Price(uint32, interface{}) (EntryDirection, uint64) - Accountable(interface{}) bool } type BalanceManager interface { //Credit is crediting the peer, charging local node - Credit(peer *Peer, amount uint64, size uint32) error + Credit(peer *Peer, amount uint64) error //Debit is crediting the local node, charging the remote peer - Debit(peer *Peer, amount uint64, size uint32) error + Debit(peer *Peer, amount uint64) error } -type EntryDirection bool +type EntryDirection uint const ( - ChargeSender EntryDirection = true - ChargeReceiver EntryDirection = false + ChargeSender EntryDirection = 1 + ChargeReceiver EntryDirection = 2 + ChargeNone EntryDirection = 3 ) type AccountingHook struct { @@ -57,14 +73,15 @@ func (ah *AccountingHook) Send(peer *Peer, size uint32, msg interface{}) error { ah.lock.Lock() defer ah.lock.Unlock() var err error - if !ah.PriceOracle.Accountable(msg) { - return nil - } direction, price := ah.PriceOracle.Price(size, msg) if direction == ChargeSender { - err = ah.BalanceManager.Debit(peer, price, size) - } else { - err = ah.BalanceManager.Credit(peer, price, size) + err = ah.BalanceManager.Debit(peer, price) + ah.debitMetrics(price, size, err) + } else if direction == ChargeReceiver { + err = ah.BalanceManager.Credit(peer, price) + ah.creditMetrics(price, size, err) + } else if direction == ChargeNone { + return nil } return err } @@ -73,14 +90,33 @@ func (ah *AccountingHook) Receive(peer *Peer, size uint32, msg interface{}) erro ah.lock.Lock() defer ah.lock.Unlock() var err error - if !ah.PriceOracle.Accountable(msg) { - return nil - } direction, price := ah.PriceOracle.Price(size, msg) if direction == ChargeReceiver { - err = ah.BalanceManager.Debit(peer, price, size) - } else { - err = ah.BalanceManager.Credit(peer, price, size) + err = ah.BalanceManager.Debit(peer, price) + ah.debitMetrics(price, size, err) + } else if direction == ChargeSender { + err = ah.BalanceManager.Credit(peer, price) + ah.creditMetrics(price, size, err) + } else if direction == ChargeNone { + return nil } return err } + +func (ah *AccountingHook) debitMetrics(price uint64, size uint32, err error) { + mBalanceDebit.Inc(int64(price)) + mBytesDebit.Inc(int64(size)) + mMsgDebit.Inc(1) + if err != nil { + mSelfDrops.Inc(1) + } +} + +func (ah *AccountingHook) creditMetrics(price uint64, size uint32, err error) { + mBalanceCredit.Inc(int64(price)) + mBytesCredit.Inc(int64(size)) + mMsgCredit.Inc(1) + if err != nil { + mPeerDrops.Inc(1) + } +} diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index ede6cea3a9..b4fb3fe9eb 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -728,24 +728,18 @@ type StreamerPriceOracle struct { registry *Registry } -func (spo *StreamerPriceOracle) Accountable(msg interface{}) bool { - code, ok := spo.registry.spec.GetCode(msg) - if !ok { - return false - } - if _, ok = spo.priceMatrix[code]; ok { - return true - } - return false -} - func (spo *StreamerPriceOracle) Price(size uint32, msg interface{}) (direction protocols.EntryDirection, price uint64) { code, ok := spo.registry.spec.GetCode(msg) + direction = protocols.ChargeNone if !ok { - panic("Attempting to get message code for an expected message type, but code not found") + //this could be handled more severely (panic) but let's be gentle? + return } - tag := spo.priceMatrix[code] + tag, ok := spo.priceMatrix[code] + if !ok { + return + } price = tag.price if tag.sizeBased { price = price * uint64(size) diff --git a/swarm/swap/api.go b/swarm/swap/api.go deleted file mode 100644 index 70aecb876c..0000000000 --- a/swarm/swap/api.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2018 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 - -import ( - "context" - "errors" - - "github.com/ethereum/go-ethereum/p2p/enode" -) - -var ( - ErrNoSuchPeerAccounting = errors.New("No accounting with that peer") -) - -//This is the API definition to access swarm swap accounting data via RPC -type API struct { - swap *Swap -} - -//Get metrics about swap for this node -//The current metrics are for accounted message types only -//(i.e. BytesTransferred is amount of bytes sent but only for a -//accounted message types) -type Metrics struct { - BalanceCredited uint64 - BalanceDebited uint64 - BytesCredited uint64 - BytesDebited uint64 - MsgCredited uint64 - MsgDebited uint64 - ChequesIssued uint64 - ChequesReceived uint64 - PeerDrops uint64 - SelfDrops uint64 -} - -//Create a new API instance -func NewAPI(swap *Swap) *API { - return &API{swap: swap} -} - -//Get the balance for this node with a specific peer -func (swapapi *API) BalanceWithPeer(ctx context.Context, peer enode.ID) (balance int64, err error) { - var ok bool - balance, ok = swapapi.swap.balances[peer] - if !ok { - err = ErrNoSuchPeerAccounting - } - return -} - -//Get the overall balance of the node -//Iterates over all peers this node is having accounted interaction -//and just adds up balances. -//It assumes that a disfavorable balance is represented as a negative value -func (swapapi *API) Balance(ctx context.Context) (balance int64, err error) { - balance = 0 - for _, peerBalance := range swapapi.swap.balances { - balance += peerBalance - } - return -} - -//Just return the Swap metrics -func (swapapi *API) GetSwapMetricsForPeer(ctx context.Context, peer enode.ID) (*Metrics, error) { - var ok bool - var metrics *Metrics - metrics, ok = swapapi.swap.metrics[peer] - if !ok { - return nil, ErrNoSuchPeerAccounting - } - return metrics, nil -} - -//Just return the Swap metrics -func (swapapi *API) GetSwapMetrics(ctx context.Context, peer enode.ID) (*Metrics, error) { - var metrics *Metrics - - for _, m := range swapapi.swap.metrics { - metrics.BalanceCredited += m.BalanceCredited - metrics.BalanceDebited += m.BalanceDebited - metrics.BytesCredited += m.BytesCredited - metrics.BytesDebited += m.BytesDebited - metrics.ChequesIssued += m.ChequesIssued - metrics.ChequesReceived += m.ChequesReceived - metrics.MsgCredited += m.MsgCredited - metrics.MsgDebited += m.MsgDebited - metrics.PeerDrops += m.PeerDrops - metrics.SelfDrops += m.SelfDrops - } - - return metrics, nil -} diff --git a/swarm/swap/chequemanager.go b/swarm/swap/chequemanager.go deleted file mode 100644 index 8e272a6426..0000000000 --- a/swarm/swap/chequemanager.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2018 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 - -import ( - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/swarm/state" -) - -type ChequeManager struct { - stateStore state.Store - serialPerNode map[enode.ID]uint64 - openDebitCheques map[enode.ID][]*Cheque - openCreditCheques map[enode.ID][]*Cheque -} - -type Cheque struct { - serial uint64 - timeout uint64 - amount int64 - sumCumulated int64 - beneficiary enode.ID //this should probably be common.Address? -} - -func NewChequeManager(stateStore state.Store) *ChequeManager { - return &ChequeManager{ - stateStore: stateStore, - //TODO: restore from state store - serialPerNode: make(map[enode.ID]uint64), - openDebitCheques: make(map[enode.ID][]*Cheque), - openCreditCheques: make(map[enode.ID][]*Cheque), - } -} - -func (mgr *ChequeManager) CreateCheque(beneficiary enode.ID, amount int64) *Cheque { - mgr.serialPerNode[beneficiary]++ - cheque := &Cheque{ - serial: mgr.serialPerNode[beneficiary], - beneficiary: beneficiary, - amount: amount, - } - openCheques := mgr.openDebitCheques[beneficiary] - if openCheques == nil { - openCheques = make([]*Cheque, 0) - } - openCheques = append(openCheques, cheque) - mgr.openDebitCheques[beneficiary] = openCheques - return cheque -} diff --git a/swarm/swap/protocol.go b/swarm/swap/protocol.go deleted file mode 100644 index 36d5448e66..0000000000 --- a/swarm/swap/protocol.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2018 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 - -import ( - "context" - "fmt" - "sync" - - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/protocols" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm/log" -) - -const ( - IsActiveProtocol = true -) - -//Here we define the Swap p2p.protocol -//We use it to standardize the interaction between nodes who need clear their accounts. -//Accounting itself is separate (see swarm/swap/swap.go) -//This protocol focuses on sending and receiving cheques and -//cheque management/clearing -//For a better understanding, please read the Swap network paper "Generalized Swap swear and swindle games" -type Protocol struct { - peersMu sync.RWMutex - peers map[enode.ID]*Peer -} - -//This is the peer representing a participant in this protocol -type Peer struct { - *protocols.Peer -} - -//Create a new protocol instance -func NewProtocol() *Protocol { - proto := &Protocol{ - peers: make(map[enode.ID]*Peer), - } - return proto -} - -// NewPeer is the constructor for the protocol Peer -func NewPeer(peer *protocols.Peer) *Peer { - p := &Peer{ - Peer: peer, - } - return p -} - -//In a peer exchange, if node A gets too indebted with node B, -//node A issues a cheque and sends it to B -type IssueChequeMsg struct { - Cheque *Cheque -} - -//In a scenario where B received a cheque from A, node B can -//redeem a cheque, which means it kicks off the process to cash it in. -//In this case, this message is sent to peer A -type RedeemChequeMsg struct { - Cheque *Cheque -} - -///////////////////////////////////////////////////////////////////// -// SECTION: node.Service interface -///////////////////////////////////////////////////////////////////// -func (s *Swap) Start(srv *p2p.Server) error { - log.Debug("Started swap") - return nil -} - -func (s *Swap) Stop() error { - log.Info("swap shutting down") - return nil -} - -var swapSpec = &protocols.Spec{ - Name: swapProtocolName, - Version: swapVersion, - MaxMsgSize: defaultMaxMsgSize, - Messages: []interface{}{ - IssueChequeMsg{}, - RedeemChequeMsg{}, - }, -} - -func (s *Swap) Protocols() []p2p.Protocol { - return []p2p.Protocol{ - { - Name: swapSpec.Name, - Version: swapSpec.Version, - Length: swapSpec.Length(), - Run: s.run, - }, - } -} - -func (s *Swap) APIs() []rpc.API { - apis := []rpc.API{ - { - Namespace: "swap", - Version: "1.0", - Service: NewAPI(s), - Public: true, - }, - } - return apis -} - -///////////////////////////////////////////////////////////////////// -// SECTION: p2p.protocol interface -///////////////////////////////////////////////////////////////////// -func (s *Swap) run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { - p := protocols.NewPeer(peer, rw, swapSpec) - sp := NewPeer(p) - s.protocol.setPeer(sp) - defer s.protocol.deletePeer(sp) - defer s.protocol.Close() - return sp.Run(sp.handleSwapMsg) -} - -func (s *Swap) NodeInfo() interface{} { - return nil -} - -func (s *Swap) PeerInfo(id enode.ID) interface{} { - return nil -} - -//------------------------------------------------------------------------------------------ -func (proto *Protocol) Close() { -} - -func (proto *Protocol) getPeer(peerId enode.ID) *Peer { - proto.peersMu.RLock() - defer proto.peersMu.RUnlock() - - return proto.peers[peerId] -} - -func (proto *Protocol) setPeer(peer *Peer) { - proto.peersMu.Lock() - defer proto.peersMu.Unlock() - - proto.peers[peer.ID()] = peer -} - -func (proto *Protocol) deletePeer(peer *Peer) { - proto.peersMu.Lock() - defer proto.peersMu.Unlock() - - delete(proto.peers, peer.ID()) -} - -func (proto *Protocol) peersCount() (c int) { - proto.peersMu.Lock() - c = len(proto.peers) - proto.peersMu.Unlock() - return -} - -//Protocol message handler for handling cheque messages -func (p *Peer) handleSwapMsg(ctx context.Context, msg interface{}) error { - switch msg := msg.(type) { - - case *IssueChequeMsg: - return p.handleIssueChequeMsg(ctx, msg) - - case *RedeemChequeMsg: - return p.handleRedeemChequeMsg(ctx, msg) - - default: - return fmt.Errorf("unknown message type: %T", msg) - } -} - -//A IssueChequeMsg has been received -func (p *Peer) handleIssueChequeMsg(ctx context.Context, msg interface{}) (err error) { - log.Debug("SwapProtocolPeer: handleIssueChequeMsg") - return err -} - -//A RedeemChequeMsg has been received -func (p *Peer) handleRedeemChequeMsg(ctx context.Context, msg interface{}) (err error) { - log.Debug("SwapProtocolPeer: handleRedeemChequeMsg") - return err -} diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index 65a9e63c51..fa7874d8a0 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -17,10 +17,8 @@ package swap import ( - "context" "errors" "fmt" - "math" "strconv" "sync" @@ -30,34 +28,18 @@ import ( "github.com/ethereum/go-ethereum/swarm/state" ) -const ( - defaultMaxMsgSize = 1024 * 1024 - swapProtocolName = "swap" - swapVersion = 1 -) - -var ( - payAt = int64(-4096 * 10000000) // threshold that triggers payment {request} (bytes) - dropAt = int64(-4096 * 12000000) // threshold that triggers disconnect (bytes) - - ErrInsufficientFunds = errors.New("Insufficient funds") -) - // SwAP Swarm Accounting Protocol // a peer to peer micropayment system // A node maintains an individual balance with every peer // Only messages which have a price will be accounted for type Swap struct { - chequeManager *ChequeManager //cheque manager keeps track of issued cheques - stateStore state.Store //stateStore is needed in order to keep balances across sessions - lock sync.RWMutex //lock the balances - balances map[enode.ID]int64 //map of balances for each peer - metrics map[enode.ID]*Metrics //map of metrics for each peer - protocol *Protocol //reference to the cheque exchange protocol + stateStore state.Store //stateStore is needed in order to keep balances across sessions + lock sync.RWMutex //lock the balances + balances map[enode.ID]int64 //map of balances for each peer } //Credit us and debit remote -func (s *Swap) Credit(peer *protocols.Peer, amount uint64, size uint32) (err error) { +func (s *Swap) Credit(peer *protocols.Peer, amount uint64) (err error) { s.lock.Lock() defer s.lock.Unlock() @@ -67,29 +49,12 @@ func (s *Swap) Credit(peer *protocols.Peer, amount uint64, size uint32) (err err peerBalance := s.balances[peer.ID()] s.stateStore.Put(peer.ID().String(), &peerBalance) - if float64(peerBalance) > math.Abs(float64(payAt)) { - ctx := context.TODO() - //WARNING: Should we do this? Otherwise anyone could create a WantChequeMsg requiring us to handle special situations... - err := s.wantCheque(ctx, peer.ID()) - if err != nil { - //TODO: special error handling, as at this point the accounting has been done - //but the cheque could not be sent? - log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) - } - } - if float64(peerBalance) > math.Abs(float64(dropAt)) { - s.metrics[peer.ID()].PeerDrops += 1 - return ErrInsufficientFunds - } - //TODO: size for metrics is currently misleading: should only account for size based messages(?) - s.updatePeerMetrics(peer, true, amount, size) - log.Debug(fmt.Sprintf("balance for peer %s: %s", peer.ID().String(), strconv.FormatInt(peerBalance, 10))) return err } //Debit us and credit remote -func (s *Swap) Debit(peer *protocols.Peer, amount uint64, size uint32) (err error) { +func (s *Swap) Debit(peer *protocols.Peer, amount uint64) (err error) { s.lock.Lock() defer s.lock.Unlock() @@ -100,23 +65,6 @@ func (s *Swap) Debit(peer *protocols.Peer, amount uint64, size uint32) (err erro peerBalance := s.balances[peer.ID()] s.stateStore.Put(peer.ID().String(), &peerBalance) - if peerBalance < payAt { - ctx := context.TODO() - err := s.issueCheque(ctx, peer.ID()) - s.metrics[peer.ID()].ChequesIssued += 1 - if err != nil { - //TODO: special error handling, as at this point the accounting has been done - //but the cheque could not be sent? - log.Warn("Payment threshold exceeded, but error sending cheque!", "err", err) - } - } - if peerBalance < dropAt { - s.metrics[peer.ID()].SelfDrops += 1 - return ErrInsufficientFunds - } - - s.updatePeerMetrics(peer, false, amount, size) - log.Debug(fmt.Sprintf("balance for peer %s: %s", peer.ID().String(), strconv.FormatInt(peerBalance, 10))) return nil } @@ -131,94 +79,21 @@ func (swap *Swap) GetPeerBalance(peer enode.ID) (int64, error) { return 0, errors.New("Peer not found") } -func (swap *Swap) GetPeerMetrics(peer enode.ID) (*Metrics, error) { - swap.lock.RLock() - defer swap.lock.RUnlock() - if p, ok := swap.metrics[peer]; ok { - return p, nil - } - return nil, errors.New("Peer not found") -} - func (s *Swap) loadState(peer *protocols.Peer) { var peerBalance int64 - var peerMetrics *Metrics peerID := peer.ID() - if _, ok := s.metrics[peerID]; !ok { - s.stateStore.Get("metrics"+peerID.String(), &peerMetrics) - if peerMetrics == nil { - peerMetrics = &Metrics{ - BalanceCredited: 0, - BalanceDebited: 0, - BytesCredited: 0, - BytesDebited: 0, - MsgCredited: 0, - MsgDebited: 0, - ChequesIssued: 0, - ChequesReceived: 0, - PeerDrops: 0, - SelfDrops: 0, - } - s.metrics[peerID] = peerMetrics - } - } if _, ok := s.balances[peerID]; !ok { s.stateStore.Get(peerID.String(), &peerBalance) s.balances[peerID] = peerBalance } } -//local node is being credited(in favor of local node), so the balance increases -func (s *Swap) updatePeerMetrics(peer *protocols.Peer, credit bool, amount uint64, size uint32) { - - metrics := s.metrics[peer.ID()] - - if credit { - metrics.BalanceCredited += amount - metrics.BytesCredited += uint64(size) - metrics.MsgCredited += 1 - } else { - metrics.BalanceDebited += amount - metrics.BytesDebited += uint64(size) - metrics.MsgDebited += 1 - } - - s.stateStore.Put("metrics"+peer.ID().String(), metrics) -} - -//Issue a cheque for the remote peer. Happens if we are indebted with the peer -//and crossed the payment threshold -func (s *Swap) issueCheque(ctx context.Context, id enode.ID) error { - cheque := s.chequeManager.CreateCheque(id, int64(math.Abs(float64(payAt)))) - _ = IssueChequeMsg{ - Cheque: cheque, - } - p := s.protocol.getPeer(id) - if p == nil { - return fmt.Errorf("wanting to send to non-connected peer!") - } - return nil - //TODO: Don't actually send any cheques yet - //return p.Send(ctx, msg) -} - -//Issue a cheque for the remote peer. Happens if we are indebted with the peer -//and crossed the payment threshold -func (s *Swap) wantCheque(ctx context.Context, id enode.ID) error { - //TODO: Don't actually initially any real message exchange yet - //return p.Send(ctx, msg) - return nil -} - // New - swap constructor func New(stateStore state.Store) (swap *Swap) { swap = &Swap{ - chequeManager: NewChequeManager(stateStore), - stateStore: stateStore, - balances: make(map[enode.ID]int64), - metrics: make(map[enode.ID]*Metrics), - protocol: NewProtocol(), + stateStore: stateStore, + balances: make(map[enode.ID]int64), } return } diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 61e6955a83..3ccc363813 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -22,29 +22,22 @@ import ( "flag" "fmt" "io/ioutil" - "math" mrand "math/rand" "os" - "path/filepath" "testing" "time" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/p2p/simulations/adapters" "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/swarm/state" colorable "github.com/mattn/go-colorable" ) var ( - p2pPort = 30100 - ipcpath = ".swarm.ipc" - datadirPrefix = ".data_" - loglevel = flag.Int("loglevel", 2, "verbosity of logs") + loglevel = flag.Int("loglevel", 2, "verbosity of logs") ) type testPriceOracle struct{} @@ -56,10 +49,6 @@ func (tpo *testPriceOracle) Accountable(msg interface{}) bool { func (tpo *testPriceOracle) Price(size uint32, msg interface{}) (protocols.EntryDirection, uint64) { switch msg := msg.(type) { - case *testExceedsPayAtMsg: - return msg.Price(size) - case *testExceedsDropAtMsg: - return msg.Price(size) case *testCheapMsg: return msg.Price(size) case *testSizeBasedMsg: @@ -67,7 +56,7 @@ func (tpo *testPriceOracle) Price(size uint32, msg interface{}) (protocols.Entry case *testChargeRecvMsg: return msg.Price(size) } - return false, 0 + return protocols.ChargeNone, 0 } var testSpec = &protocols.Spec{ @@ -75,8 +64,6 @@ var testSpec = &protocols.Spec{ Version: 1, MaxMsgSize: 10 * 1024 * 1024, Messages: []interface{}{ - testExceedsPayAtMsg{}, - testExceedsDropAtMsg{}, testCheapMsg{}, testSizeBasedMsg{}, testChargeRecvMsg{}, @@ -102,24 +89,12 @@ func (d *dummyRW) ReadMsg() (p2p.Msg, error) { } //define a couple of messages for tests -type testExceedsPayAtMsg struct{} -type testExceedsDropAtMsg struct{} type testCheapMsg struct{} type testSizeBasedMsg struct { Data []byte } type testChargeRecvMsg struct{} -//this message is just one unit more expensive than the payment threshold -func (tmsg *testExceedsPayAtMsg) Price(size uint32) (protocols.EntryDirection, uint64) { - return protocols.ChargeSender, uint64(math.Abs(float64(payAt)) + float64(1)) -} - -//this message is just one unit more expensive than the disconnect threshold -func (tmsg *testExceedsDropAtMsg) Price(size uint32) (protocols.EntryDirection, uint64) { - return protocols.ChargeSender, uint64(math.Abs(float64(dropAt)) + float64(1)) -} - //a message with an arbitrary cost func (tmsg *testCheapMsg) Price(size uint32) (protocols.EntryDirection, uint64) { return protocols.ChargeSender, uint64(10) @@ -142,13 +117,6 @@ func init() { log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) } -//check that the disconnect threshold is below the payment threshold -func TestLimits(t *testing.T) { - if dropAt >= payAt { - t.Fatal(fmt.Sprintf("dropAt limit is not lower than payAt limit, dropAt: %d, payAt: %d", dropAt, payAt)) - } -} - //check that the disconnect threshold is below the payment threshold func TestRepeatedBookings(t *testing.T) { //create a test swap account @@ -156,12 +124,10 @@ func TestRepeatedBookings(t *testing.T) { defer os.RemoveAll(testDir) testPeer := newDummyPeer() - //size is irrelevant for this test - size := uint32(0) amount := mrand.Intn(100) cnt := 1 + mrand.Intn(10) for i := 0; i < cnt; i++ { - swap.Credit(testPeer.Peer.Peer, uint64(amount), size) + swap.Credit(testPeer.Peer, uint64(amount)) } expectedBalance := int64(cnt * amount) realBalance := swap.balances[testPeer.ID()] @@ -173,7 +139,7 @@ func TestRepeatedBookings(t *testing.T) { amount = mrand.Intn(100) cnt = 1 + mrand.Intn(10) for i := 0; i < cnt; i++ { - swap.Debit(testPeer2.Peer.Peer, uint64(amount), size) + swap.Debit(testPeer2.Peer, uint64(amount)) } expectedBalance = int64(0 - (cnt * amount)) realBalance = swap.balances[testPeer2.ID()] @@ -185,9 +151,9 @@ func TestRepeatedBookings(t *testing.T) { amount1 := mrand.Intn(100) amount2 := mrand.Intn(100) amount3 := mrand.Intn(100) - swap.Credit(testPeer2.Peer.Peer, uint64(amount1), size) - swap.Credit(testPeer2.Peer.Peer, uint64(amount2), size) - swap.Debit(testPeer2.Peer.Peer, uint64(amount3), size) + swap.Credit(testPeer2.Peer, uint64(amount1)) + swap.Credit(testPeer2.Peer, uint64(amount2)) + swap.Debit(testPeer2.Peer, uint64(amount3)) expectedBalance = expectedBalance + int64(amount1+amount2-amount3) realBalance = swap.balances[testPeer2.ID()] @@ -197,66 +163,6 @@ func TestRepeatedBookings(t *testing.T) { } } -//unit test for exceeds pay limit -//when the payment threshold is reached, a cheque will be issued -//this test checks that a cheque is present if a message is sent -//which exceeds the payment threshold -//(note: the details of cheque handling will need to be fleshed out -//in future iterations, current implementation is very primitive) -func TestExceedsPayAt(t *testing.T) { - //create a test swap account - swap, testDir := createTestSwap(t) - defer os.RemoveAll(testDir) - - msg := &testExceedsPayAtMsg{} - ctx := context.Background() - testPeer := newDummyPeer() - testPeer.Send(ctx, msg) - - _, price := msg.Price(0) - if swap.balances[testPeer.ID()] != int64(0-price) { - t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) - } - - //check that a cheque has been created - cheques := swap.chequeManager.openDebitCheques[testPeer.ID()] - if cheques == nil { - t.Fatal("Expected cheques for this peer to be present, but are nil") - } - if len(cheques) == 0 { - t.Fatal("Expected a cheque to have arrived, but len is zero") - } - if cheques[0].serial != 1 { - t.Fatal(fmt.Sprintf("Expected the serial to be one (first message but is: %d", cheques[0].serial)) - } - if float64(cheques[0].amount) != float64(math.Abs(float64(payAt))) { - t.Fatal(fmt.Sprintf("Expected cheques amount to be equal to payAt limit, but it is: %d", cheques[0].amount)) - } -} - -//unit test for exceeds drop limit -//tests that a message is being sent which crosses the drop limit -//in that case, we should receive a InsufficientFunds error -func TestExceedsDropAt(t *testing.T) { - //create a test swap account - swap, testDir := createTestSwap(t) - defer os.RemoveAll(testDir) - - msg := &testExceedsDropAtMsg{} - ctx := context.Background() - testPeer := newDummyPeer() - err := testPeer.Send(ctx, msg) - - _, price := msg.Price(0) - if swap.balances[testPeer.ID()] != int64(0-price) { - t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) - } - - if err != ErrInsufficientFunds { - t.Fatal("Expected this test to fail with insufficient funds, but it did not") - } -} - //send a message with cost, //then check that the balance has the expected amount func TestSendCheapMessage(t *testing.T) { @@ -400,106 +306,12 @@ func dummyMsgHandler(ctx context.Context, msg interface{}) error { return nil } -func createAndStartSvcNode(swap *Swap, t *testing.T) *node.Node { - stack, err := newServiceNode(p2pPort, 0, 0) - if err != nil { - t.Fatal("Create servicenode #1 fail", "err", err) - } - - swapsvc := func(ctx *node.ServiceContext) (node.Service, error) { - return swap, nil - } - - err = stack.Register(swapsvc) - if err != nil { - t.Fatal("Register service in servicenode #1 fail", "err", err) - } - - // start the nodes - err = stack.Start() - if err != nil { - t.Fatal("servicenode #1 start failed", "err", err) - } - - return stack -} - -//tests some basic things over RPC -func TestSwapRPC(t *testing.T) { - - swap, testDir := createTestSwap(t) - defer os.RemoveAll(testDir) - - stack := createAndStartSvcNode(swap, t) - defer stack.Stop() - defer os.RemoveAll(stack.DataDir()) - - // connect to the servicenode RPCs - rpcclient, err := rpc.Dial(filepath.Join(stack.DataDir(), ipcpath)) - if err != nil { - t.Fatal("connect to servicenode IPC fail", "err", err) - } - defer os.RemoveAll(stack.DataDir()) - - var balance int64 - err = rpcclient.Call(&balance, "swap_balance") - if err != nil { - t.Fatal("servicenode RPC failed", "err", err) - } - log.Debug("servicenode balance", "balance", balance) - - if balance != 0 { - t.Fatal("Expected balance to be 0 but it is not") - } - - dummyPeer1 := newDummyPeer() - dummyPeer2 := newDummyPeer() - id1 := dummyPeer1.ID() - id2 := dummyPeer2.ID() - - fake1 := int64(234) - fake2 := int64(-100) - - swap.balances[id1] = fake1 - swap.balances[id2] = fake2 - - err = rpcclient.Call(&balance, "swap_balanceWithPeer", id1) - if err != nil { - t.Fatal("servicenode RPC failed", "err", err) - } - log.Debug("balance1", "balance-1", balance) - if balance != fake1 { - t.Fatal(fmt.Sprintf("Expected balance %d to be equal to fake balance %d, but it is not", balance, fake1)) - } - - err = rpcclient.Call(&balance, "swap_balanceWithPeer", id2) - if err != nil { - t.Fatal("servicenode RPC failed", "err", err) - } - log.Debug("balance2", "balance-2", balance) - if balance != fake2 { - t.Fatal(fmt.Sprintf("Expected balance %d to be equal to fake balance %d, but it is not", balance, fake2)) - } - - err = rpcclient.Call(&balance, "swap_balance") - if err != nil { - t.Fatal("servicenode RPC failed", "err", err) - } - log.Debug("balance", "balance", balance) - - fakeSum := fake1 + fake2 - if balance != fakeSum { - t.Fatal(fmt.Sprintf("Expected balance %d to be equal to sum %d, but it is not", balance, fakeSum)) - } -} - type dummyPeer struct { - *Peer + *protocols.Peer testFunc func(error) } func (dp *dummyPeer) Drop(err error) { - fmt.Println("DD") dp.testFunc(err) } @@ -508,34 +320,7 @@ func newDummyPeer() *dummyPeer { id := adapters.RandomNodeConfig().ID protoPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), &dummyRW{}, testSpec) dummy := &dummyPeer{ - Peer: NewPeer(protoPeer), + Peer: protoPeer, } return dummy } - -//creates a p2p.Service node stub -func newServiceNode(port int, httpport int, wsport int, modules ...string) (*node.Node, error) { - cfg := &node.DefaultConfig - cfg.P2P.ListenAddr = fmt.Sprintf(":%d", port) - cfg.P2P.EnableMsgEvents = true - cfg.P2P.NoDiscovery = true - cfg.IPCPath = ipcpath - cfg.DataDir = fmt.Sprintf("%s%d", datadirPrefix, port) - if httpport > 0 { - cfg.HTTPHost = node.DefaultHTTPHost - cfg.HTTPPort = httpport - } - if wsport > 0 { - cfg.WSHost = node.DefaultWSHost - cfg.WSPort = wsport - cfg.WSOrigins = []string{"*"} - for i := 0; i < len(modules); i++ { - cfg.WSModules = append(cfg.WSModules, modules[i]) - } - } - stack, err := node.New(cfg) - if err != nil { - return nil, fmt.Errorf("ServiceNode create fail: %v", err) - } - return stack, nil -} diff --git a/swarm/swap_test.go b/swarm/swap_test.go index dc47ffd85e..82aad6939e 100644 --- a/swarm/swap_test.go +++ b/swarm/swap_test.go @@ -29,6 +29,7 @@ import ( "time" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/simulations/adapters" @@ -172,23 +173,6 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { } else { log.Debug(fmt.Sprintf("Node %s has no balance with node %s", node.TerminalString(), n.TerminalString())) } - metrics, err := swarm.swap.GetPeerMetrics(n) - if err == nil && *printStats { - fmt.Println(fmt.Sprintf("********** Metrics for node %s with node %s: *************", node.TerminalString(), n.TerminalString())) - fmt.Println(fmt.Sprintf("Total units credited: %d", metrics.BalanceCredited)) - fmt.Println(fmt.Sprintf("Total units debited: %d", metrics.BalanceDebited)) - fmt.Println(fmt.Sprintf("Total bytes credited: %d", metrics.BytesCredited)) - fmt.Println(fmt.Sprintf("Total bytes debited: %d", metrics.BytesDebited)) - fmt.Println(fmt.Sprintf("Cheques issued: %d", metrics.ChequesIssued)) - fmt.Println(fmt.Sprintf("Cheques received: %d", metrics.ChequesReceived)) - fmt.Println(fmt.Sprintf("Number of messages credited: %d", metrics.MsgCredited)) - fmt.Println(fmt.Sprintf("Number of messages debited: %d", metrics.MsgDebited)) - fmt.Println(fmt.Sprintf("Peers dropped: %d", metrics.PeerDrops)) - fmt.Println(fmt.Sprintf("Number of times node dropped itself: %d", metrics.SelfDrops)) - } else { - //not all peers have metrics with every node, so probably can be ignored - log.Debug("Error getting metrics", "err", err) - } } //update the map for this node balancesMap[node] = subBalances @@ -202,6 +186,17 @@ func TestSwapNetworkSymmetricFileUpload(t *testing.T) { fmt.Println(fmt.Sprintf(".........with node %s: balance %d", kk.TerminalString(), vv)) } } + //NOTE: this are currently metrics over ALL nodes, not per node + fmt.Println(fmt.Sprintf("Total units credited: %d", metrics.Get("account.balance.credit").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Total units debited: %d", metrics.Get("account.balance.debit").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Total bytes credited: %d", metrics.Get("account.bytes.credit").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Total bytes debited: %d", metrics.Get("account.bytes.debit").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Cheques issued: %d", metrics.Get("account.cheques.issued").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Cheques received: %d", metrics.Get("account.cheques.received").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Number of messages credited: %d", metrics.Get("account.msg.credit").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Number of messages debited: %d", metrics.Get("account.msg.debit").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Peers dropped: %d", metrics.Get("account.peerdrops").(metrics.Counter).Count())) + fmt.Println(fmt.Sprintf("Number of times node dropped itself: %d", metrics.Get("account.selfdrops").(metrics.Counter).Count())) } //now iterate the whole map diff --git a/swarm/swarm.go b/swarm/swarm.go index 8c0a327fad..24bbfa6418 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -367,10 +367,6 @@ func (self *Swarm) Start(srv *p2p.Server) error { } log.Info("Swarm network started", "bzzaddr", fmt.Sprintf("%x", self.bzz.Hive.BaseAddr())) - if self.swap != nil { - self.swap.Start(srv) - } - if self.ps != nil { self.ps.Start(srv) } @@ -450,9 +446,6 @@ func (self *Swarm) Protocols() (protos []p2p.Protocol) { protos = append(protos, self.ps.Protocols()...) } - if self.swap != nil { - protos = append(protos, self.swap.Protocols()...) - } return } From 9f82f9bea5b8faf0225d47b666781c24c4b30a03 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Wed, 10 Oct 2018 16:56:01 -0500 Subject: [PATCH 27/30] p2p/protocols: unit tests for accounting hooks --- p2p/protocols/accounting_test.go | 128 +++++++++++++++++++++++++++++++ p2p/protocols/protocol.go | 2 + 2 files changed, 130 insertions(+) create mode 100644 p2p/protocols/accounting_test.go diff --git a/p2p/protocols/accounting_test.go b/p2p/protocols/accounting_test.go new file mode 100644 index 0000000000..2319b28681 --- /dev/null +++ b/p2p/protocols/accounting_test.go @@ -0,0 +1,128 @@ +// Copyright 2018 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 protocols + +import ( + "bytes" + "context" + "errors" + "testing" + "time" + + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/simulations/adapters" + "github.com/ethereum/go-ethereum/rlp" +) + +var ( + wouldHaveAccounted = errors.New("ignore this error") +) + +type dummy struct { + content string +} + +//dummy implementation of a MsgReadWriter +//this allows for quick and easy unit tests without +//having to build up the complete protocol +type dummyRW struct{} + +func (d *dummyRW) WriteMsg(msg p2p.Msg) error { + return nil +} + +func (d *dummyRW) ReadMsg() (p2p.Msg, error) { + return p2p.Msg{ + Code: 0, + Size: 5, + Payload: bytes.NewReader(getDummyMsg()), + ReceivedAt: time.Now(), + }, nil +} + +func getDummyMsg() []byte { + msg := &dummy{content: "test"} + r, _ := rlp.EncodeToBytes(msg) + + var b bytes.Buffer + wmsg := WrappedMsg{ + Context: b.Bytes(), + Size: uint32(len(r)), + Payload: r, + } + + rr, _ := rlp.EncodeToBytes(wmsg) + return rr +} + +func createTestSpec() *Spec { + spec := &Spec{ + Name: "test", + Version: 42, + MaxMsgSize: 10 * 1024, + Messages: []interface{}{ + dummy{}, + }, + } + return spec +} + +type dummyBalanceMgr struct{} +type dummyPriceOracle struct{} + +func (d *dummyPriceOracle) Price(uint32, interface{}) (EntryDirection, uint64) { + return ChargeSender, 99 +} + +func (d *dummyBalanceMgr) Credit(peer *Peer, amount uint64) error { + return wouldHaveAccounted +} + +func (d *dummyBalanceMgr) Debit(peer *Peer, amount uint64) error { + return wouldHaveAccounted +} + +// Test that passing a nil hook doesn't affect sending +func TestProtocolNilHook(t *testing.T) { + spec := createTestSpec() + id := adapters.RandomNodeConfig().ID + p := p2p.NewPeer(id, "testPeer", nil) + peer := NewPeer(p, &dummyRW{}, spec) + + peer.Send(context.Background(), dummy{}) + peer.handleIncoming(func(ctx context.Context, msg interface{}) error { + return nil + }) +} + +func TestProtocolHook(t *testing.T) { + spec := createTestSpec() + spec.Hook = NewAccountingHook(&dummyBalanceMgr{}, &dummyPriceOracle{}) + id := adapters.RandomNodeConfig().ID + p := p2p.NewPeer(id, "testPeer", nil) + peer := NewPeer(p, &dummyRW{}, spec) + + err := peer.Send(context.Background(), dummy{}) + if err == nil || err != wouldHaveAccounted { + t.Fatal("Expected fake accounting to happen, but didn't") + } + + err = peer.handleIncoming(nil) + if err == nil || err != wouldHaveAccounted { + t.Fatal("Expected fake accounting to happen, but didn't") + } +} diff --git a/p2p/protocols/protocol.go b/p2p/protocols/protocol.go index 5cbc254bca..f3908bf940 100644 --- a/p2p/protocols/protocol.go +++ b/p2p/protocols/protocol.go @@ -32,6 +32,7 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" "io" "reflect" @@ -355,6 +356,7 @@ func (p *Peer) handleIncoming(handle func(ctx context.Context, msg interface{}) if p.spec.Hook != nil { if wmsg.Size != uint32(len(wmsg.Payload)) { log.Warn("Advertised message size and payload length don't match") + p.Drop(errors.New("message size and payload length don't match")) } err := p.spec.Hook.Receive(p, wmsg.Size, val) if err != nil { From 9f3a6643125976831e5a55182dbbf8cb2df23003 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 11 Oct 2018 19:01:58 -0500 Subject: [PATCH 28/30] p2p/protocols: optimized accounting --- p2p/protocols/accounting.go | 126 +++++++++--------- p2p/protocols/accounting_test.go | 211 ++++++++++++++++++++++--------- p2p/protocols/protocol_test.go | 133 +++++++++++++++++++ swarm/network/stream/stream.go | 60 ++++----- swarm/swap/swap.go | 21 +-- swarm/swap/swap_test.go | 200 ++--------------------------- 6 files changed, 385 insertions(+), 366 deletions(-) diff --git a/p2p/protocols/accounting.go b/p2p/protocols/accounting.go index 2319b6a99b..74f8cd2d7b 100644 --- a/p2p/protocols/accounting.go +++ b/p2p/protocols/accounting.go @@ -17,8 +17,6 @@ package protocols import ( - "sync" - "github.com/ethereum/go-ethereum/metrics" ) @@ -36,87 +34,87 @@ var ( mChequesReceived = metrics.NewRegisteredCounterForced("account.cheques.received", nil) ) -type PriceOracle interface { - Price(uint32, interface{}) (EntryDirection, uint64) +type Prices interface { + Price(interface{}) *Price } -type BalanceManager interface { - //Credit is crediting the peer, charging local node - Credit(peer *Peer, amount uint64) error - //Debit is crediting the local node, charging the remote peer - Debit(peer *Peer, amount uint64) error +type Price struct { + Value int64 //Positive if sender pays, negative if receiver pays + PerByte bool //True if the price is per byte or for unit } -type EntryDirection uint - -const ( - ChargeSender EntryDirection = 1 - ChargeReceiver EntryDirection = 2 - ChargeNone EntryDirection = 3 -) - -type AccountingHook struct { - BalanceManager - PriceOracle - lock sync.RWMutex //lock the balances +func (p *Price) ForSender(size uint32) int64 { + price := p.Value + if p.PerByte { + price *= int64(size) + } + return price } -func NewAccountingHook(mgr BalanceManager, po PriceOracle) *AccountingHook { - ah := &AccountingHook{ - PriceOracle: po, - BalanceManager: mgr, +func (p *Price) ForReceiver(size uint32) int64 { + price := p.Value + if p.PerByte { + price *= int64(size) + } + return 0 - price +} + +type Balance interface { + //Adds amount to the local balance with remote node `peer`; + //positive amount = credit local node + //negative amount = debit local node + Add(amount int64, peer *Peer) error +} + +type Accounting struct { + Balance + Prices +} + +func NewAccounting(mgr Balance, po Prices) *Accounting { + ah := &Accounting{ + Prices: po, + Balance: mgr, } return ah } -func (ah *AccountingHook) Send(peer *Peer, size uint32, msg interface{}) error { - ah.lock.Lock() - defer ah.lock.Unlock() - var err error - direction, price := ah.PriceOracle.Price(size, msg) - if direction == ChargeSender { - err = ah.BalanceManager.Debit(peer, price) - ah.debitMetrics(price, size, err) - } else if direction == ChargeReceiver { - err = ah.BalanceManager.Credit(peer, price) - ah.creditMetrics(price, size, err) - } else if direction == ChargeNone { +func (ah *Accounting) Send(peer *Peer, size uint32, msg interface{}) error { + price := ah.Price(msg) + if price == nil { return nil } + finalPrice := price.ForSender(size) + err := ah.Add(finalPrice, peer) + ah.doMetrics(finalPrice, size, err) return err } -func (ah *AccountingHook) Receive(peer *Peer, size uint32, msg interface{}) error { - ah.lock.Lock() - defer ah.lock.Unlock() - var err error - direction, price := ah.PriceOracle.Price(size, msg) - if direction == ChargeReceiver { - err = ah.BalanceManager.Debit(peer, price) - ah.debitMetrics(price, size, err) - } else if direction == ChargeSender { - err = ah.BalanceManager.Credit(peer, price) - ah.creditMetrics(price, size, err) - } else if direction == ChargeNone { +func (ah *Accounting) Receive(peer *Peer, size uint32, msg interface{}) error { + price := ah.Price(msg) + if price == nil { return nil } + finalPrice := price.ForReceiver(size) + err := ah.Add(finalPrice, peer) + ah.doMetrics(finalPrice, size, err) return err } -func (ah *AccountingHook) debitMetrics(price uint64, size uint32, err error) { - mBalanceDebit.Inc(int64(price)) - mBytesDebit.Inc(int64(size)) - mMsgDebit.Inc(1) - if err != nil { - mSelfDrops.Inc(1) - } -} - -func (ah *AccountingHook) creditMetrics(price uint64, size uint32, err error) { - mBalanceCredit.Inc(int64(price)) - mBytesCredit.Inc(int64(size)) - mMsgCredit.Inc(1) - if err != nil { - mPeerDrops.Inc(1) +func (ah *Accounting) doMetrics(price int64, size uint32, err error) { + if price > 0 { + mBalanceCredit.Inc(int64(price)) + mBytesCredit.Inc(int64(size)) + mMsgCredit.Inc(1) + if err != nil { + mPeerDrops.Inc(1) + } + } else { + mBalanceDebit.Inc(int64(price)) + mBytesDebit.Inc(int64(size)) + mMsgDebit.Inc(1) + if err != nil { + mSelfDrops.Inc(1) + } } } diff --git a/p2p/protocols/accounting_test.go b/p2p/protocols/accounting_test.go index 2319b28681..9c16699bfc 100644 --- a/p2p/protocols/accounting_test.go +++ b/p2p/protocols/accounting_test.go @@ -19,7 +19,6 @@ package protocols import ( "bytes" "context" - "errors" "testing" "time" @@ -28,44 +27,176 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) -var ( - wouldHaveAccounted = errors.New("ignore this error") -) +type dummyBalance struct { + amount int64 + peer *Peer +} -type dummy struct { - content string +type dummyPrices struct{} + +type perBytesMsg struct { + Content string +} + +type perUnitMsg struct{} +type zeroMsg struct{} + +func (d *dummyPrices) Price(msg interface{}) *Price { + switch msg.(type) { + case *perBytesMsg: + return &Price{ + PerByte: true, + Value: int64(100), + } + case *perUnitMsg: + return &Price{ + PerByte: false, + Value: int64(-99), + } + } + return nil +} + +func (d *dummyBalance) Add(amount int64, peer *Peer) error { + d.amount = amount + d.peer = peer + return nil +} + +func TestNoHook(t *testing.T) { + spec := createTestSpec() + id := adapters.RandomNodeConfig().ID + p := p2p.NewPeer(id, "testPeer", nil) + peer := NewPeer(p, &dummyRW{}, spec) + ctx := context.TODO() + msg := &perBytesMsg{Content: "testBalance"} + peer.Send(ctx, msg) + peer.handleIncoming(func(ctx context.Context, msg interface{}) error { + return nil + }) +} + +func TestSendBalance(t *testing.T) { + balance := &dummyBalance{} + prices := &dummyPrices{} + + spec := createTestSpec() + spec.Hook = NewAccounting(balance, prices) + + id := adapters.RandomNodeConfig().ID + p := p2p.NewPeer(id, "testPeer", nil) + peer := NewPeer(p, &dummyRW{}, spec) + ctx := context.TODO() + msg := &perBytesMsg{Content: "testBalance"} + size, _ := rlp.EncodeToBytes(msg) + peer.Send(ctx, msg) + if balance.amount != int64((len(size) * 100)) { + t.Fatalf("Expected price to be %d but is %d", (len(size) * 100), balance.amount) + } + + msg2 := &perUnitMsg{} + peer.Send(ctx, msg2) + if balance.amount != int64(-99) { + t.Fatalf("Expected price to be %d but is %d", -99, balance.amount) + } + + balance.amount = 77 + msg3 := &zeroMsg{} + peer.Send(ctx, msg3) + if balance.amount != int64(77) { + t.Fatalf("Expected price to be %d but is %d", 77, balance.amount) + } +} + +func TestReceiveBalance(t *testing.T) { + balance := &dummyBalance{} + prices := &dummyPrices{} + + spec := createTestSpec() + spec.Hook = NewAccounting(balance, prices) + + id := adapters.RandomNodeConfig().ID + p := p2p.NewPeer(id, "testPeer", nil) + rw := &dummyRW{} + peer := NewPeer(p, rw, spec) + msg := &perBytesMsg{Content: "testBalance"} + size, _ := rlp.EncodeToBytes(msg) + + rw.msg = msg + rw.code, _ = spec.GetCode(msg) + err := peer.handleIncoming(func(ctx context.Context, msg interface{}) error { + return nil + }) + if err != nil { + t.Fatalf("Expected no error, but got error: %v", err) + } + if balance.amount != int64((len(size) * (-100))) { + t.Fatalf("Expected price to be %d but is %d", (len(size) * (-100)), balance.amount) + } + + msg2 := &perUnitMsg{} + rw.msg = msg2 + rw.code, _ = spec.GetCode(msg2) + err = peer.handleIncoming(func(ctx context.Context, msg interface{}) error { + return nil + }) + if err != nil { + t.Fatalf("Expected no error, but got error: %v", err) + } + if balance.amount != int64(99) { + t.Fatalf("Expected price to be %d but is %d", 99, balance.amount) + } + + msg3 := &zeroMsg{} + rw.msg = msg3 + rw.code, _ = spec.GetCode(msg3) + //need to reset cause no accounting won't overwrite + balance.amount = -888 + err = peer.handleIncoming(func(ctx context.Context, msg interface{}) error { + return nil + }) + if err != nil { + t.Fatalf("Expected no error, but got error: %v", err) + } + + if balance.amount != int64(-888) { + t.Fatalf("Expected price to be %d but is %d", -888, balance.amount) + } } //dummy implementation of a MsgReadWriter //this allows for quick and easy unit tests without //having to build up the complete protocol -type dummyRW struct{} +type dummyRW struct { + msg interface{} + size uint32 + code uint64 +} func (d *dummyRW) WriteMsg(msg p2p.Msg) error { return nil } func (d *dummyRW) ReadMsg() (p2p.Msg, error) { + enc := bytes.NewReader(d.getDummyMsg()) return p2p.Msg{ - Code: 0, - Size: 5, - Payload: bytes.NewReader(getDummyMsg()), + Code: d.code, + Size: d.size, + Payload: enc, ReceivedAt: time.Now(), }, nil } -func getDummyMsg() []byte { - msg := &dummy{content: "test"} - r, _ := rlp.EncodeToBytes(msg) - +func (d *dummyRW) getDummyMsg() []byte { + r, _ := rlp.EncodeToBytes(d.msg) var b bytes.Buffer wmsg := WrappedMsg{ Context: b.Bytes(), Size: uint32(len(r)), Payload: r, } - rr, _ := rlp.EncodeToBytes(wmsg) + d.size = uint32(len(rr)) return rr } @@ -75,54 +206,10 @@ func createTestSpec() *Spec { Version: 42, MaxMsgSize: 10 * 1024, Messages: []interface{}{ - dummy{}, + perBytesMsg{}, + perUnitMsg{}, + zeroMsg{}, }, } return spec } - -type dummyBalanceMgr struct{} -type dummyPriceOracle struct{} - -func (d *dummyPriceOracle) Price(uint32, interface{}) (EntryDirection, uint64) { - return ChargeSender, 99 -} - -func (d *dummyBalanceMgr) Credit(peer *Peer, amount uint64) error { - return wouldHaveAccounted -} - -func (d *dummyBalanceMgr) Debit(peer *Peer, amount uint64) error { - return wouldHaveAccounted -} - -// Test that passing a nil hook doesn't affect sending -func TestProtocolNilHook(t *testing.T) { - spec := createTestSpec() - id := adapters.RandomNodeConfig().ID - p := p2p.NewPeer(id, "testPeer", nil) - peer := NewPeer(p, &dummyRW{}, spec) - - peer.Send(context.Background(), dummy{}) - peer.handleIncoming(func(ctx context.Context, msg interface{}) error { - return nil - }) -} - -func TestProtocolHook(t *testing.T) { - spec := createTestSpec() - spec.Hook = NewAccountingHook(&dummyBalanceMgr{}, &dummyPriceOracle{}) - id := adapters.RandomNodeConfig().ID - p := p2p.NewPeer(id, "testPeer", nil) - peer := NewPeer(p, &dummyRW{}, spec) - - err := peer.Send(context.Background(), dummy{}) - if err == nil || err != wouldHaveAccounted { - t.Fatal("Expected fake accounting to happen, but didn't") - } - - err = peer.handleIncoming(nil) - if err == nil || err != wouldHaveAccounted { - t.Fatal("Expected fake accounting to happen, but didn't") - } -} diff --git a/p2p/protocols/protocol_test.go b/p2p/protocols/protocol_test.go index 4755db3e62..4d704d5691 100644 --- a/p2p/protocols/protocol_test.go +++ b/p2p/protocols/protocol_test.go @@ -197,6 +197,139 @@ func TestProtoHandshakeSuccess(t *testing.T) { runProtoHandshake(t, &protoHandshake{42, "420"}) } +type dummyHook struct { + peer *Peer + size uint32 + msg interface{} + send bool + err error + waitC chan struct{} +} + +type dummyMsg struct { + Content string +} + +func (d *dummyHook) Send(peer *Peer, size uint32, msg interface{}) error { + d.peer = peer + d.size = size + d.msg = msg + d.send = true + return d.err +} + +func (d *dummyHook) Receive(peer *Peer, size uint32, msg interface{}) error { + d.peer = peer + d.size = size + d.msg = msg + d.send = false + d.waitC <- struct{}{} + return d.err +} + +func TestProtocolHook(t *testing.T) { + testHook := &dummyHook{ + waitC: make(chan struct{}, 1), + } + spec := &Spec{ + Name: "test", + Version: 42, + MaxMsgSize: 10 * 1024, + Messages: []interface{}{ + dummyMsg{}, + }, + Hook: testHook, + } + + runFunc := func(p *p2p.Peer, rw p2p.MsgReadWriter) error { + peer := NewPeer(p, rw, spec) + ctx := context.TODO() + peer.Send(ctx, &dummyMsg{ + Content: "handshake"}) + + handle := func(ctx context.Context, msg interface{}) error { + return nil + } + + return peer.Run(handle) + } + + conf := adapters.RandomNodeConfig() + tester := p2ptest.NewProtocolTester(t, conf.ID, 2, runFunc) + err := tester.TestExchanges(p2ptest.Exchange{ + Expects: []p2ptest.Expect{ + { + Code: 0, + Msg: &dummyMsg{Content: "handshake"}, + Peer: tester.Nodes[0].ID(), + }, + }, + }) + if err != nil { + t.Fatal(err) + } + if testHook.msg == nil || testHook.msg.(*dummyMsg).Content != "handshake" { + t.Fatal("Expected msg to be set, but it is not") + } + if testHook.send != true { + t.Fatal("Expected a send message, but it is not") + } + if testHook.peer == nil || testHook.peer.ID() != tester.Nodes[0].ID() { + t.Fatal("Expected peer ID to be set correctly, but it is not") + } + if testHook.size != 11 { //11 is the length of the encoded message + t.Fatalf("Expected size to be %d, but it is %d ", 1, testHook.size) + } + + err = tester.TestExchanges(p2ptest.Exchange{ + Triggers: []p2ptest.Trigger{ + { + Code: 0, + Msg: &dummyMsg{Content: "response"}, + Peer: tester.Nodes[1].ID(), + }, + }, + }) + + <-testHook.waitC + + if err != nil { + t.Fatal(err) + } + if testHook.msg == nil || testHook.msg.(*dummyMsg).Content != "response" { + t.Fatal("Expected msg to be set, but it is not") + } + if testHook.send != false { + t.Fatal("Expected a send message, but it is not") + } + if testHook.peer == nil || testHook.peer.ID() != tester.Nodes[1].ID() { + t.Fatal("Expected peer ID to be set correctly, but it is not") + } + if testHook.size != 10 { //11 is the length of the encoded message + t.Fatalf("Expected size to be %d, but it is %d ", 1, testHook.size) + } + + testHook.err = fmt.Errorf("dummy error") + err = tester.TestExchanges(p2ptest.Exchange{ + Triggers: []p2ptest.Trigger{ + { + Code: 0, + Msg: &dummyMsg{Content: "response"}, + Peer: tester.Nodes[1].ID(), + }, + }, + }) + + <-testHook.waitC + + time.Sleep(100 * time.Millisecond) + err = tester.TestDisconnected(&p2ptest.Disconnect{tester.Nodes[1].ID(), testHook.err}) + if err != nil { + t.Fatalf("Expected a specific disconnect error, but got different one: %v", err) + } + +} + func moduleHandshakeExchange(id enode.ID, resp uint) []p2ptest.Exchange { return []p2ptest.Exchange{ diff --git a/swarm/network/stream/stream.go b/swarm/network/stream/stream.go index b4fb3fe9eb..46340c94c8 100644 --- a/swarm/network/stream/stream.go +++ b/swarm/network/stream/stream.go @@ -62,8 +62,8 @@ type Registry struct { intervalsStore state.Store doRetrieve bool maxPeerServers int - balanceMgr protocols.BalanceManager - priceOracle protocols.PriceOracle + balanceMgr protocols.Balance + prices protocols.Prices spec *protocols.Spec } @@ -77,7 +77,7 @@ type RegistryOptions struct { } // NewRegistry is Streamer constructor -func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.SyncChunkStore, intervalsStore state.Store, options *RegistryOptions, balanceMgr protocols.BalanceManager) *Registry { +func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.SyncChunkStore, intervalsStore state.Store, options *RegistryOptions, balanceMgr protocols.Balance) *Registry { if options == nil { options = &RegistryOptions{} } @@ -194,8 +194,8 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy func (r *Registry) setupSpec() { r.createSpec() r.createPriceOracle() - if !reflect.ValueOf(r.balanceMgr).IsNil() { - r.spec.Hook = protocols.NewAccountingHook(r.balanceMgr, r.priceOracle) + if r.balanceMgr != nil && !reflect.ValueOf(r.balanceMgr).IsNil() { + r.spec.Hook = protocols.NewAccounting(r.balanceMgr, r.prices) } } @@ -718,65 +718,53 @@ func (r *Registry) createSpec() { //An accountable message needs some meta information attached to it //in order to evaluate the correct price -type priceTag struct { - price uint64 - sizeBased bool - direction protocols.EntryDirection -} -type StreamerPriceOracle struct { - priceMatrix map[uint64]*priceTag +type StreamerPrices struct { + priceMatrix map[uint64]*protocols.Price registry *Registry } -func (spo *StreamerPriceOracle) Price(size uint32, msg interface{}) (direction protocols.EntryDirection, price uint64) { +func (spo *StreamerPrices) Price(msg interface{}) *protocols.Price { code, ok := spo.registry.spec.GetCode(msg) - direction = protocols.ChargeNone if !ok { //this could be handled more severely (panic) but let's be gentle? - return + return nil } - tag, ok := spo.priceMatrix[code] + price, ok := spo.priceMatrix[code] if !ok { - return + return nil } - price = tag.price - if tag.sizeBased { - price = price * uint64(size) - } - direction = tag.direction - return + + return price } func (r *Registry) createPriceOracle() { - po := &StreamerPriceOracle{ + po := &StreamerPrices{ registry: r, } - po.priceMatrix = make(map[uint64]*priceTag) + po.priceMatrix = make(map[uint64]*protocols.Price) deliveryCode, ok := r.spec.GetCode(ChunkDeliveryMsgRetrieval{}) if !ok { panic("Attempting to get message code for an expected message type, but code not found") } - tag := &priceTag{ - price: uint64(100), - sizeBased: true, - direction: protocols.ChargeReceiver, + price := &protocols.Price{ + Value: int64(-100), + PerByte: true, } - po.priceMatrix[deliveryCode] = tag + po.priceMatrix[deliveryCode] = price retrieveReqCode, ok := r.spec.GetCode(RetrieveRequestMsg{}) if !ok { panic("Attempting to get message code for an expected message type, but code not found") } - tag = &priceTag{ - price: uint64(10), - sizeBased: false, - direction: protocols.ChargeSender, + price = &protocols.Price{ + Value: int64(10), + PerByte: false, } - po.priceMatrix[retrieveReqCode] = tag - r.priceOracle = po + po.priceMatrix[retrieveReqCode] = price + r.prices = po } func (r *Registry) Protocols() []p2p.Protocol { diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index fa7874d8a0..bff207a983 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -39,13 +39,14 @@ type Swap struct { } //Credit us and debit remote -func (s *Swap) Credit(peer *protocols.Peer, amount uint64) (err error) { +func (s *Swap) Add(amount int64, peer *protocols.Peer) (err error) { s.lock.Lock() defer s.lock.Unlock() s.loadState(peer) - s.balances[peer.ID()] += int64(amount) + s.balances[peer.ID()] += amount + peerBalance := s.balances[peer.ID()] s.stateStore.Put(peer.ID().String(), &peerBalance) @@ -53,22 +54,6 @@ func (s *Swap) Credit(peer *protocols.Peer, amount uint64) (err error) { return err } -//Debit us and credit remote -func (s *Swap) Debit(peer *protocols.Peer, amount uint64) (err error) { - s.lock.Lock() - defer s.lock.Unlock() - - s.loadState(peer) - - //local node is being debited (in favor of remote peer), so its balance decreases - s.balances[peer.ID()] -= int64(amount) - peerBalance := s.balances[peer.ID()] - s.stateStore.Put(peer.ID().String(), &peerBalance) - - log.Debug(fmt.Sprintf("balance for peer %s: %s", peer.ID().String(), strconv.FormatInt(peerBalance, 10))) - return nil -} - //get a peer's balance func (swap *Swap) GetPeerBalance(peer enode.ID) (int64, error) { swap.lock.RLock() diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 3ccc363813..2708725c38 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -17,21 +17,17 @@ package swap import ( - "context" - "crypto/rand" "flag" "fmt" "io/ioutil" mrand "math/rand" "os" "testing" - "time" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/protocols" "github.com/ethereum/go-ethereum/p2p/simulations/adapters" - "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/swarm/state" colorable "github.com/mattn/go-colorable" ) @@ -40,76 +36,6 @@ var ( loglevel = flag.Int("loglevel", 2, "verbosity of logs") ) -type testPriceOracle struct{} - -func (tpo *testPriceOracle) Accountable(msg interface{}) bool { - _, ok := testSpec.GetCode(msg) - return ok -} - -func (tpo *testPriceOracle) Price(size uint32, msg interface{}) (protocols.EntryDirection, uint64) { - switch msg := msg.(type) { - case *testCheapMsg: - return msg.Price(size) - case *testSizeBasedMsg: - return msg.Price(size) - case *testChargeRecvMsg: - return msg.Price(size) - } - return protocols.ChargeNone, 0 -} - -var testSpec = &protocols.Spec{ - Name: "swapTestSpec", - Version: 1, - MaxMsgSize: 10 * 1024 * 1024, - Messages: []interface{}{ - testCheapMsg{}, - testSizeBasedMsg{}, - testChargeRecvMsg{}, - }, -} - -//dummy implementation of a MsgReadWriter -//this allows for quick and easy unit tests without -//having to build up the complete protocol -type dummyRW struct{} - -func (d *dummyRW) WriteMsg(msg p2p.Msg) error { - return nil -} - -func (d *dummyRW) ReadMsg() (p2p.Msg, error) { - return p2p.Msg{ - Code: 0, - Size: 0, - Payload: nil, - ReceivedAt: time.Now(), - }, nil -} - -//define a couple of messages for tests -type testCheapMsg struct{} -type testSizeBasedMsg struct { - Data []byte -} -type testChargeRecvMsg struct{} - -//a message with an arbitrary cost -func (tmsg *testCheapMsg) Price(size uint32) (protocols.EntryDirection, uint64) { - return protocols.ChargeSender, uint64(10) -} - -//a message which needs to be charged based on price -func (tmsg *testSizeBasedMsg) Price(size uint32) (protocols.EntryDirection, uint64) { - return protocols.ChargeSender, uint64(size) * uint64(100) -} - -//a message which needs to be charged based on price -func (tmsg *testChargeRecvMsg) Price(size uint32) (protocols.EntryDirection, uint64) { - return protocols.ChargeReceiver, uint64(999) -} - func init() { flag.Parse() @@ -127,7 +53,7 @@ func TestRepeatedBookings(t *testing.T) { amount := mrand.Intn(100) cnt := 1 + mrand.Intn(10) for i := 0; i < cnt; i++ { - swap.Credit(testPeer.Peer, uint64(amount)) + swap.Add(int64(amount), testPeer.Peer) } expectedBalance := int64(cnt * amount) realBalance := swap.balances[testPeer.ID()] @@ -139,7 +65,7 @@ func TestRepeatedBookings(t *testing.T) { amount = mrand.Intn(100) cnt = 1 + mrand.Intn(10) for i := 0; i < cnt; i++ { - swap.Debit(testPeer2.Peer, uint64(amount)) + swap.Add(0-int64(amount), testPeer2.Peer) } expectedBalance = int64(0 - (cnt * amount)) realBalance = swap.balances[testPeer2.ID()] @@ -149,13 +75,13 @@ func TestRepeatedBookings(t *testing.T) { //mixed debits and credits amount1 := mrand.Intn(100) - amount2 := mrand.Intn(100) - amount3 := mrand.Intn(100) - swap.Credit(testPeer2.Peer, uint64(amount1)) - swap.Credit(testPeer2.Peer, uint64(amount2)) - swap.Debit(testPeer2.Peer, uint64(amount3)) + amount2 := mrand.Intn(55) + amount3 := mrand.Intn(999) + swap.Add(int64(amount1), testPeer2.Peer) + swap.Add(int64(0-amount2), testPeer2.Peer) + swap.Add(int64(0-amount3), testPeer2.Peer) - expectedBalance = expectedBalance + int64(amount1+amount2-amount3) + expectedBalance = expectedBalance + int64(amount1-amount2-amount3) realBalance = swap.balances[testPeer2.ID()] if expectedBalance != realBalance { @@ -163,25 +89,6 @@ func TestRepeatedBookings(t *testing.T) { } } -//send a message with cost, -//then check that the balance has the expected amount -func TestSendCheapMessage(t *testing.T) { - //create a test swap account - swap, testDir := createTestSwap(t) - defer os.RemoveAll(testDir) - - msg := &testCheapMsg{} - ctx := context.Background() - testPeer := newDummyPeer() - testPeer.Send(ctx, msg) - - //check the new balance - _, price := msg.Price(0) - if swap.balances[testPeer.ID()] != int64(0-price) { - t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) - } -} - //try restoring a balance from state store //this is simulated by creating a node, //assigning it an arbitrary balance, @@ -193,25 +100,11 @@ func TestRestoreBalanceFromStateStore(t *testing.T) { swap, testDir := createTestSwap(t) defer os.RemoveAll(testDir) - msg := &testCheapMsg{} - ctx := context.Background() testPeer := newDummyPeer() - testPeer.Send(ctx, msg) + swap.balances[testPeer.ID()] = -8888 - //check the new balance - _, price := msg.Price(0) - expectedBalance := int64(0 - price) - if swap.balances[testPeer.ID()] != expectedBalance { - t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) - } - - var tmpBalance int64 - swap.stateStore.Get(testPeer.ID().String(), &tmpBalance) - //compare the balances - if expectedBalance != tmpBalance { - t.Fatal(fmt.Sprintf("Unexpected balance value in stateStore after sending cheap message test - stateStore should have saved it. Expected balance: %d, balance is: %d", - expectedBalance, tmpBalance)) - } + tmpBalance := swap.balances[testPeer.ID()] + swap.stateStore.Put(testPeer.ID().String(), &tmpBalance) swap.stateStore.Close() swap.stateStore = nil @@ -225,63 +118,9 @@ func TestRestoreBalanceFromStateStore(t *testing.T) { stateStore.Get(testPeer.ID().String(), &newBalance) //compare the balances - if expectedBalance != newBalance { + if tmpBalance != newBalance { t.Fatal(fmt.Sprintf("Unexpected balance value after sending cheap message test. Expected balance: %d, balance is: %d", - expectedBalance, newBalance)) - } -} - -//send a message with cost, -//then check that the balance has the expected amount -//the message is charged size based -func TestSendSizeBasedMsg(t *testing.T) { - //create a test swap account - swap, testDir := createTestSwap(t) - defer os.RemoveAll(testDir) - - msg := &testSizeBasedMsg{} - msg.Data = make([]byte, 16) - rand.Read(msg.Data) - ctx := context.Background() - testPeer := newDummyPeer() - testPeer.Send(ctx, msg) - - //check the new balance - r, err := rlp.EncodeToBytes(msg) - if err != nil { - t.Fatal(err) - } - - size := uint32(len(r)) - _, price := msg.Price(size) - expectedBalance := int64(0 - price) - expectedPrice := uint64(size * 100) - if price != expectedPrice { - t.Fatalf("Expected price to be %d, but is %d", expectedPrice, price) - } - if swap.balances[testPeer.ID()] != expectedBalance { - t.Fatalf("Expected balance to be %d, but is %d", expectedBalance, swap.balances[testPeer.ID()]) - } -} - -//charge as being the receiver of a receiver-pays message -//as this test for simplicity only sends messages, -//it means that the balance is changed in positive, -//instead of negative as with all other tests -func TestChargeAsReceiver(t *testing.T) { - //create a test swap account - swap, testDir := createTestSwap(t) - defer os.RemoveAll(testDir) - - msg := &testChargeRecvMsg{} - ctx := context.Background() - testPeer := newDummyPeer() - testPeer.Send(ctx, msg) - - //check the new balance - _, price := msg.Price(0) - if swap.balances[testPeer.ID()] != int64(price) { - t.Fatalf("Expected balance to be %d, but is %d", price, swap.balances[testPeer.ID()]) + tmpBalance, newBalance)) } } @@ -297,28 +136,17 @@ func createTestSwap(t *testing.T) (*Swap, string) { t.Fatal(err2) } swap := New(stateStore) - testSpec.Hook = protocols.NewAccountingHook(swap, &testPriceOracle{}) return swap, dir } -//dummy message handler (needed or we will have a panic in the accounting) -func dummyMsgHandler(ctx context.Context, msg interface{}) error { - return nil -} - type dummyPeer struct { *protocols.Peer - testFunc func(error) -} - -func (dp *dummyPeer) Drop(err error) { - dp.testFunc(err) } //creates a dummy protocols.Peer with dummy MsgReadWriter func newDummyPeer() *dummyPeer { id := adapters.RandomNodeConfig().ID - protoPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), &dummyRW{}, testSpec) + protoPeer := protocols.NewPeer(p2p.NewPeer(id, "testPeer", nil), nil, nil) dummy := &dummyPeer{ Peer: protoPeer, } From 86b421d8e1bca07bf19f27b6e0898d3ff9ac9878 Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Thu, 11 Oct 2018 19:09:28 -0500 Subject: [PATCH 29/30] swarm/swap: rebased on latest master --- swarm/network/stream/snapshot_retrieval_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swarm/network/stream/snapshot_retrieval_test.go b/swarm/network/stream/snapshot_retrieval_test.go index 09d915d48d..83529aea1e 100644 --- a/swarm/network/stream/snapshot_retrieval_test.go +++ b/swarm/network/stream/snapshot_retrieval_test.go @@ -130,7 +130,7 @@ func retrievalStreamerFunc(ctx *adapters.ServiceContext, bucket *sync.Map) (s no DoSync: true, SyncUpdateDelay: 3 * time.Second, DoRetrieve: true, - }) + }, nil) fileStore := storage.NewFileStore(netStore, storage.NewFileStoreParams()) bucket.Store(bucketKeyFileStore, fileStore) From a222ed4fa06fc2d8b47a175d021cb1fabf23f12c Mon Sep 17 00:00:00 2001 From: Fabio Barone Date: Fri, 12 Oct 2018 11:18:27 -0500 Subject: [PATCH 30/30] swarm/swap, p2p/protocols: finalized first phase of swap accounting --- p2p/protocols/accounting.go | 63 +++++++++++--- p2p/protocols/accounting_test.go | 142 ++++++++++++++++++++++++++++--- p2p/protocols/protocol.go | 17 +++- swarm/swap/swap.go | 14 ++- swarm/swap/swap_test.go | 2 +- 5 files changed, 203 insertions(+), 35 deletions(-) diff --git a/p2p/protocols/accounting.go b/p2p/protocols/accounting.go index 74f8cd2d7b..67a8f20384 100644 --- a/p2p/protocols/accounting.go +++ b/p2p/protocols/accounting.go @@ -20,29 +20,47 @@ import ( "github.com/ethereum/go-ethereum/metrics" ) +//define some metrics var ( //NOTE: these metrics just define the interfaces and are currently *NOT persisted* over sessions - mBalanceCredit = metrics.NewRegisteredCounterForced("account.balance.credit", nil) - mBalanceDebit = metrics.NewRegisteredCounterForced("account.balance.debit", nil) - mBytesCredit = metrics.NewRegisteredCounterForced("account.bytes.credit", nil) - mBytesDebit = metrics.NewRegisteredCounterForced("account.bytes.debit", nil) - mMsgCredit = metrics.NewRegisteredCounterForced("account.msg.credit", nil) - mMsgDebit = metrics.NewRegisteredCounterForced("account.msg.debit", nil) - mPeerDrops = metrics.NewRegisteredCounterForced("account.peerdrops", nil) - mSelfDrops = metrics.NewRegisteredCounterForced("account.selfdrops", nil) - mChequesIssued = metrics.NewRegisteredCounterForced("account.cheques.issued", nil) - mChequesReceived = metrics.NewRegisteredCounterForced("account.cheques.received", nil) + //All metrics are cumulative + + //total amount of units credited + mBalanceCredit = metrics.NewRegisteredCounterForced("account.balance.credit", nil) + //total amount of units debited + mBalanceDebit = metrics.NewRegisteredCounterForced("account.balance.debit", nil) + //total amount of bytes credited + mBytesCredit = metrics.NewRegisteredCounterForced("account.bytes.credit", nil) + //total amount of bytes debited + mBytesDebit = metrics.NewRegisteredCounterForced("account.bytes.debit", nil) + //total amount of credited messages + mMsgCredit = metrics.NewRegisteredCounterForced("account.msg.credit", nil) + //total amount of debited messages + mMsgDebit = metrics.NewRegisteredCounterForced("account.msg.debit", nil) + //how many times local node had to drop remote peers + mPeerDrops = metrics.NewRegisteredCounterForced("account.peerdrops", nil) + //how many times local node overdrafted and dropped + mSelfDrops = metrics.NewRegisteredCounterForced("account.selfdrops", nil) + //how many cheques have been issued + //mChequesIssued = metrics.NewRegisteredCounterForced("account.cheques.issued", nil) + //how many cheques have been received + //mChequesReceived = metrics.NewRegisteredCounterForced("account.cheques.received", nil) ) +//Prices defines how prices are being passed on to the accounting instance type Prices interface { + //Return the Price for a message Price(interface{}) *Price } +//Price represents the costs of a message type Price struct { Value int64 //Positive if sender pays, negative if receiver pays PerByte bool //True if the price is per byte or for unit } +//Price can be different depending on if it is the sender or receiver who pays +//ForSender gives back the price for sending a message func (p *Price) ForSender(size uint32) int64 { price := p.Value if p.PerByte { @@ -51,6 +69,8 @@ func (p *Price) ForSender(size uint32) int64 { return price } +//Price can be different depending on if it is the sender or receiver who pays +//ForReceiver gives back the price for receiving a message func (p *Price) ForReceiver(size uint32) int64 { price := p.Value if p.PerByte { @@ -59,6 +79,9 @@ func (p *Price) ForReceiver(size uint32) int64 { return 0 - price } +//Balance is the actual accounting instance +//Balance defines the operations needed for accounting +//Implementations internally maintain the balance for every peer type Balance interface { //Adds amount to the local balance with remote node `peer`; //positive amount = credit local node @@ -66,9 +89,12 @@ type Balance interface { Add(amount int64, peer *Peer) error } +//Accounting implements the Hook interface +//It interfaces to the balances through the Balance interface, +//while interfacing with protocols and its prices through the Prices interface type Accounting struct { - Balance - Prices + Balance //interface to accounting logic + Prices //interface to prices logic } func NewAccounting(mgr Balance, po Prices) *Accounting { @@ -79,28 +105,41 @@ func NewAccounting(mgr Balance, po Prices) *Accounting { return ah } +//Implement Hook.Send func (ah *Accounting) Send(peer *Peer, size uint32, msg interface{}) error { + //get the price for a message (through the protocol spec) price := ah.Price(msg) + //this message doesn't need accounting if price == nil { return nil } + //evaluate the price for sending messages finalPrice := price.ForSender(size) + //do the accounting err := ah.Add(finalPrice, peer) + //record metrics ah.doMetrics(finalPrice, size, err) return err } +//Implement Hook.Receive func (ah *Accounting) Receive(peer *Peer, size uint32, msg interface{}) error { + //get the price for a message (through the protocol spec) price := ah.Price(msg) + //this message doesn't need accounting if price == nil { return nil } + //evaluate the price for receiving messages finalPrice := price.ForReceiver(size) + //do the accounting err := ah.Add(finalPrice, peer) + //record metrics ah.doMetrics(finalPrice, size, err) return err } +//record some metrics func (ah *Accounting) doMetrics(price int64, size uint32, err error) { if price > 0 { mBalanceCredit.Inc(int64(price)) diff --git a/p2p/protocols/accounting_test.go b/p2p/protocols/accounting_test.go index 9c16699bfc..7e4f85446e 100644 --- a/p2p/protocols/accounting_test.go +++ b/p2p/protocols/accounting_test.go @@ -27,27 +27,36 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +//dummy Balance implementation type dummyBalance struct { amount int64 peer *Peer } +//dummy Prices implementation type dummyPrices struct{} +//a dummy message which needs size based accounting type perBytesMsg struct { Content string } +//a dummy message which is paid for per unit type perUnitMsg struct{} + +//a dummy message which doesn't need any payment type zeroMsg struct{} +//return the price for the defined messages func (d *dummyPrices) Price(msg interface{}) *Price { switch msg.(type) { + //size based message cost, sender pays case *perBytesMsg: return &Price{ PerByte: true, Value: int64(100), } + //unitary cost, receiver pays case *perUnitMsg: return &Price{ PerByte: false, @@ -57,71 +66,173 @@ func (d *dummyPrices) Price(msg interface{}) *Price { return nil } +//dummy accounting implementation, only stores values for later check func (d *dummyBalance) Add(amount int64, peer *Peer) error { d.amount = amount d.peer = peer return nil } +//We need to test that if the hook is not defined, then message infrastructure +//(send,receive) still works func TestNoHook(t *testing.T) { + //create a test spec spec := createTestSpec() + //a random node id := adapters.RandomNodeConfig().ID + //a peer p := p2p.NewPeer(id, "testPeer", nil) - peer := NewPeer(p, &dummyRW{}, spec) + rw := &dummyRW{} + peer := NewPeer(p, rw, spec) ctx := context.TODO() msg := &perBytesMsg{Content: "testBalance"} + //send a message peer.Send(ctx, msg) + //simulate receiving a message + rw.msg = msg peer.handleIncoming(func(ctx context.Context, msg interface{}) error { return nil }) + //all should just work and not result in any error } -func TestSendBalance(t *testing.T) { +//lowest level unit test +func TestSend(t *testing.T) { + //create instances balance := &dummyBalance{} prices := &dummyPrices{} - + //create the spec spec := createTestSpec() + //create the accounting hook for the spec spec.Hook = NewAccounting(balance, prices) + //create a peer + id := adapters.RandomNodeConfig().ID + p := p2p.NewPeer(id, "testPeer", nil) + peer := NewPeer(p, &dummyRW{}, spec) + msg := &perBytesMsg{Content: "testBalance"} + size, _ := rlp.EncodeToBytes(msg) + spec.Hook.Send(peer, uint32(len(size)), msg) + if balance.amount != int64(len(size)*100) { + t.Fatalf("Expected balance to be %d but is %d", (len(size) * 100), balance.amount) + } + + //send a unitary, receiver pays message + msg2 := &perUnitMsg{} + spec.Hook.Send(peer, 0, msg2) + //check that the balance is actually the expected + if balance.amount != int64(-99) { + t.Fatalf("Expected balance to be %d but is %d", -99, balance.amount) + } + + //set arbitrary balance + balance.amount = 77 + //send a non accounted message, balance should not change + msg3 := &zeroMsg{} + spec.Hook.Send(peer, 0, msg3) + if balance.amount != int64(77) { + t.Fatalf("Expected balance to be %d but is %d", 77, balance.amount) + } +} + +//lowest level unit test +func TestReceive(t *testing.T) { + //create instances + balance := &dummyBalance{} + prices := &dummyPrices{} + //create the spec + spec := createTestSpec() + //create the accounting hook for the spec + spec.Hook = NewAccounting(balance, prices) + //create a peer + id := adapters.RandomNodeConfig().ID + p := p2p.NewPeer(id, "testPeer", nil) + peer := NewPeer(p, &dummyRW{}, spec) + msg := &perBytesMsg{Content: "testBalance"} + + size, _ := rlp.EncodeToBytes(msg) + spec.Hook.Receive(peer, uint32(len(size)), msg) + if balance.amount != int64(len(size)*-100) { + t.Fatalf("Expected balance to be %d but is %d", (len(size) * 100), balance.amount) + } + + //send a unitary, receiver pays message + msg2 := &perUnitMsg{} + spec.Hook.Receive(peer, 0, msg2) + //check that the balance is actually the expected + if balance.amount != int64(99) { + t.Fatalf("Expected balance to be %d but is %d", -99, balance.amount) + } + + //set arbitrary balance + balance.amount = 77 + //send a non accounted message, balance should not change + msg3 := &zeroMsg{} + spec.Hook.Receive(peer, 0, msg3) + if balance.amount != int64(77) { + t.Fatalf("Expected balance to be %d but is %d", 77, balance.amount) + } +} + +//Test sending with a peer, higher level +func TestSendWithPeer(t *testing.T) { + //create instances + balance := &dummyBalance{} + prices := &dummyPrices{} + //create the spec + spec := createTestSpec() + //create the accounting hook for the spec + spec.Hook = NewAccounting(balance, prices) + //create a peer id := adapters.RandomNodeConfig().ID p := p2p.NewPeer(id, "testPeer", nil) peer := NewPeer(p, &dummyRW{}, spec) ctx := context.TODO() msg := &perBytesMsg{Content: "testBalance"} size, _ := rlp.EncodeToBytes(msg) + //send a size based message peer.Send(ctx, msg) + //check that the balance is actually the expected if balance.amount != int64((len(size) * 100)) { - t.Fatalf("Expected price to be %d but is %d", (len(size) * 100), balance.amount) + t.Fatalf("Expected balance to be %d but is %d", (len(size) * 100), balance.amount) } + //send a unitary, receiver pays message msg2 := &perUnitMsg{} peer.Send(ctx, msg2) + //check that the balance is actually the expected if balance.amount != int64(-99) { - t.Fatalf("Expected price to be %d but is %d", -99, balance.amount) + t.Fatalf("Expected balance to be %d but is %d", -99, balance.amount) } + //set arbitrary balance balance.amount = 77 + //send a non accounted message, balance should not change msg3 := &zeroMsg{} peer.Send(ctx, msg3) if balance.amount != int64(77) { - t.Fatalf("Expected price to be %d but is %d", 77, balance.amount) + t.Fatalf("Expected balance to be %d but is %d", 77, balance.amount) } } -func TestReceiveBalance(t *testing.T) { +//Test receiving with a peer, higher level +func TestReceiveWithPeer(t *testing.T) { + //create instances balance := &dummyBalance{} prices := &dummyPrices{} - + //create the spec spec := createTestSpec() + //create the accounting hook for the spec spec.Hook = NewAccounting(balance, prices) - + //create a peer id := adapters.RandomNodeConfig().ID p := p2p.NewPeer(id, "testPeer", nil) rw := &dummyRW{} peer := NewPeer(p, rw, spec) + + //simulate receiving a size based, sender-pays message msg := &perBytesMsg{Content: "testBalance"} size, _ := rlp.EncodeToBytes(msg) - rw.msg = msg rw.code, _ = spec.GetCode(msg) err := peer.handleIncoming(func(ctx context.Context, msg interface{}) error { @@ -131,9 +242,10 @@ func TestReceiveBalance(t *testing.T) { t.Fatalf("Expected no error, but got error: %v", err) } if balance.amount != int64((len(size) * (-100))) { - t.Fatalf("Expected price to be %d but is %d", (len(size) * (-100)), balance.amount) + t.Fatalf("Expected balance to be %d but is %d", (len(size) * (-100)), balance.amount) } + //simulate receiving a size based, sender-pays message msg2 := &perUnitMsg{} rw.msg = msg2 rw.code, _ = spec.GetCode(msg2) @@ -144,23 +256,24 @@ func TestReceiveBalance(t *testing.T) { t.Fatalf("Expected no error, but got error: %v", err) } if balance.amount != int64(99) { - t.Fatalf("Expected price to be %d but is %d", 99, balance.amount) + t.Fatalf("Expected balance to be %d but is %d", 99, balance.amount) } + //set arbitrary balance an msg3 := &zeroMsg{} rw.msg = msg3 rw.code, _ = spec.GetCode(msg3) //need to reset cause no accounting won't overwrite balance.amount = -888 + //simulate receiving a non accounted message, balance should not change err = peer.handleIncoming(func(ctx context.Context, msg interface{}) error { return nil }) if err != nil { t.Fatalf("Expected no error, but got error: %v", err) } - if balance.amount != int64(-888) { - t.Fatalf("Expected price to be %d but is %d", -888, balance.amount) + t.Fatalf("Expected balance to be %d but is %d", -888, balance.amount) } } @@ -200,6 +313,7 @@ func (d *dummyRW) getDummyMsg() []byte { return rr } +//create a test spec func createTestSpec() *Spec { spec := &Spec{ Name: "test", diff --git a/p2p/protocols/protocol.go b/p2p/protocols/protocol.go index f3908bf940..4376f2b3a6 100644 --- a/p2p/protocols/protocol.go +++ b/p2p/protocols/protocol.go @@ -123,9 +123,14 @@ type WrappedMsg struct { Payload []byte } +//For accounting, the design is to allow the Spec to describe which and how its messages are priced +//To access this functionality, we provide a Hook interface which will call accounting methods +//NOTE: there could be more such (horizontal) hooks in the future type Hook interface { - Send(*Peer, uint32, interface{}) error - Receive(*Peer, uint32, interface{}) error + //A hook for sending messages + Send(peer *Peer, size uint32, msg interface{}) error + //A hook for receiving messages + Receive(peer *Peer, size uint32, msg interface{}) error } // Spec is a protocol specification including its name and version as well as @@ -147,6 +152,7 @@ type Spec struct { // each message must have a single unique data type Messages []interface{} + //hook for accounting (could be extended to multiple hooks in the future) Hook Hook initOnce sync.Once @@ -287,6 +293,7 @@ func (p *Peer) Send(ctx context.Context, msg interface{}) error { return errorf(ErrInvalidMsgType, "%v", code) } + //if the accounting hook is set, call it if p.spec.Hook != nil { err := p.spec.Hook.Send(p, wmsg.Size, msg) if err != nil { @@ -353,10 +360,12 @@ func (p *Peer) handleIncoming(handle func(ctx context.Context, msg interface{}) return errorf(ErrDecode, "<= %v: %v", msg, err) } + //if the accounting hook is set, call it if p.spec.Hook != nil { if wmsg.Size != uint32(len(wmsg.Payload)) { - log.Warn("Advertised message size and payload length don't match") - p.Drop(errors.New("message size and payload length don't match")) + errMsg := "Advertised message size and payload length don't match" + log.Warn(errMsg) + p.Drop(errors.New(errMsg)) } err := p.spec.Hook.Receive(p, wmsg.Size, val) if err != nil { diff --git a/swarm/swap/swap.go b/swarm/swap/swap.go index bff207a983..e610e1483b 100644 --- a/swarm/swap/swap.go +++ b/swarm/swap/swap.go @@ -38,15 +38,18 @@ type Swap struct { balances map[enode.ID]int64 //map of balances for each peer } -//Credit us and debit remote +//Swap implements the protocols.Balance interface +//Add is the (sole) accounting function func (s *Swap) Add(amount int64, peer *protocols.Peer) (err error) { s.lock.Lock() defer s.lock.Unlock() + //load existing balances from the state store s.loadState(peer) - + //adjust the balance + //if amount is negative, it will decrease, otherwise increase s.balances[peer.ID()] += amount - + //save the new balance to the state store peerBalance := s.balances[peer.ID()] s.stateStore.Put(peer.ID().String(), &peerBalance) @@ -54,7 +57,7 @@ func (s *Swap) Add(amount int64, peer *protocols.Peer) (err error) { return err } -//get a peer's balance +//Get a peer's balance func (swap *Swap) GetPeerBalance(peer enode.ID) (int64, error) { swap.lock.RLock() defer swap.lock.RUnlock() @@ -64,9 +67,12 @@ func (swap *Swap) GetPeerBalance(peer enode.ID) (int64, error) { return 0, errors.New("Peer not found") } +//load balances from the state store (persisted) func (s *Swap) loadState(peer *protocols.Peer) { var peerBalance int64 peerID := peer.ID() + //only load if the current instance doesn't already have this peer's + //balance in memory if _, ok := s.balances[peerID]; !ok { s.stateStore.Get(peerID.String(), &peerBalance) s.balances[peerID] = peerBalance diff --git a/swarm/swap/swap_test.go b/swarm/swap/swap_test.go index 2708725c38..d1b48a2f4f 100644 --- a/swarm/swap/swap_test.go +++ b/swarm/swap/swap_test.go @@ -43,7 +43,7 @@ func init() { log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) } -//check that the disconnect threshold is below the payment threshold +//Test that repeated bookings do correct accounting func TestRepeatedBookings(t *testing.T) { //create a test swap account swap, testDir := createTestSwap(t)