Skip to content
This repository has been archived by the owner on Aug 2, 2021. It is now read-only.

Swap available balance #1892

Merged
merged 20 commits into from
Oct 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f0431f4
swap: introduce paidOut variable and update this variable upon sendin…
Eknir Oct 18, 2019
de5b152
swap: make tests pass again
Eknir Oct 18, 2019
18500a9
swap, contract/swap: create event iterators on Deposit and Withdraw a…
Eknir Oct 18, 2019
d11b4b3
swap: add availableBalance to swapAPI
Eknir Oct 18, 2019
d4e5abc
contracts/swap: add Withdraw and Deposit methods
Eknir Oct 18, 2019
1ff69ff
contracts/swap: add Deposit and Withdraw to Contract interface
Eknir Oct 18, 2019
6eb0a57
swap: added draft for TestAvailableBalance (not working yet)
Eknir Oct 18, 2019
523d220
swap: compute available balance upon startup and update the value on …
Eknir Oct 21, 2019
075c322
swap: finish TestAvailableBalance
Eknir Oct 21, 2019
7807019
swap: review mortelli
Eknir Oct 21, 2019
e0cab6a
contracts/swap: check error before returning
Eknir Oct 21, 2019
203dbec
swap: small improvements
Eknir Oct 22, 2019
f52e515
swap, contracts/swap: compute availableBalance without event watchers
Eknir Oct 23, 2019
57416dd
Merge branch 'master' into swap-available-balance
Eknir Oct 23, 2019
4011ffe
swap: fix TestAvailableBalance
Eknir Oct 23, 2019
80296ac
contracts/swap: add amount as a parameter to deposit
Eknir Oct 24, 2019
cacb39f
contracts/swap: don't allow auth.value in Deposit
Eknir Oct 28, 2019
ed886ab
Merge branch 'master' into swap-available-balance
Eknir Oct 28, 2019
5bc0c28
contracts/swap: comment LiquidBalance function
Eknir Oct 29, 2019
c11f840
Merge branch 'master' into swap-available-balance
Eknir Oct 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions contracts/swap/swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package swap
import (
"context"
"errors"
"fmt"
"math/big"
"time"

Expand All @@ -44,8 +45,14 @@ type Backend interface {

// Contract interface defines the methods exported from the underlying go-bindings for the smart contract
type Contract interface {
// Withdraw attempts to withdraw Wei from the chequebook
Withdraw(auth *bind.TransactOpts, backend Backend, amount *big.Int) (*types.Receipt, error)
mortelli marked this conversation as resolved.
Show resolved Hide resolved
// Deposit sends a raw transaction to the chequebook, triggering the fallback—depositing amount
Deposit(auth *bind.TransactOpts, backend Backend, amout *big.Int) (*types.Receipt, error)
// CashChequeBeneficiary cashes the cheque by the beneficiary
CashChequeBeneficiary(auth *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*CashChequeResult, *types.Receipt, error)
// LiquidBalance returns the LiquidBalance (total balance in Wei - total hard deposits in Wei) of the chequebook
LiquidBalance(auth *bind.CallOpts) (*big.Int, error)
// ContractParams returns contract info (e.g. deployed address)
ContractParams() *Params
// Issuer returns the contract owner from the blockchain
Expand Down Expand Up @@ -97,6 +104,32 @@ func InstanceAt(address common.Address, backend Backend) (Contract, error) {
return c, err
}

// Withdraw withdraws amount from the chequebook and blocks until the transaction is mined
func (s simpleContract) Withdraw(auth *bind.TransactOpts, backend Backend, amount *big.Int) (*types.Receipt, error) {
tx, err := s.instance.Withdraw(auth, amount)
if err != nil {
return nil, err
}
return WaitFunc(auth, backend, tx)
}

// Deposit sends a transaction to the chequebook, which deposits the amount set in Auth.Value and blocks until the transaction is mined
func (s simpleContract) Deposit(auth *bind.TransactOpts, backend Backend, amount *big.Int) (*types.Receipt, error) {
rawSimpleSwap := contract.SimpleSwapRaw{Contract: s.instance}
if auth.Value != big.NewInt(0) {
return nil, fmt.Errorf("Deposit value can only be set via amount parameter")
}
if amount == big.NewInt(0) {
return nil, fmt.Errorf("Deposit amount cannot be equal to zero")
}
auth.Value = amount
mortelli marked this conversation as resolved.
Show resolved Hide resolved
tx, err := rawSimpleSwap.Transfer(auth)
mortelli marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
return WaitFunc(auth, backend, tx)
}

// CashChequeBeneficiary cashes the cheque on the blockchain and blocks until the transaction is mined.
func (s simpleContract) CashChequeBeneficiary(opts *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*CashChequeResult, *types.Receipt, error) {
tx, err := s.instance.CashChequeBeneficiary(opts, beneficiary, cumulativePayout, ownerSig)
Expand Down Expand Up @@ -131,6 +164,11 @@ func (s simpleContract) CashChequeBeneficiary(opts *bind.TransactOpts, beneficia
return result, receipt, nil
}

// LiquidBalance returns the LiquidBalance (total balance in Wei - total hard deposits in Wei) of the chequebook
func (s simpleContract) LiquidBalance(opts *bind.CallOpts) (*big.Int, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment exported func

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

return s.instance.LiquidBalance(opts)
}

// ContractParams returns contract information
func (s simpleContract) ContractParams() *Params {
return &Params{
Expand Down
1 change: 1 addition & 0 deletions swap/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ type swapAPI interface {
SentCheques() (map[enode.ID]*Cheque, error)
ReceivedCheque(peer enode.ID) (cheque *Cheque, err error)
ReceivedCheques() (map[enode.ID]*Cheque, error)
AvailableBalance() (uint64, error)
}

// API would be the API accessor for protocol methods
Expand Down
59 changes: 49 additions & 10 deletions swap/swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ const swapLogLevel = 3 // swapLogLevel indicates filter level of log messages
// A node maintains an individual balance with every peer
// Only messages which have a price will be accounted for
type Swap struct {
store state.Store // store is needed in order to keep balances and cheques across sessions
peers map[enode.ID]*Peer // map of all swap Peers
peersLock sync.RWMutex // lock for peers map
backend contract.Backend // the backend (blockchain) used
owner *Owner // contract access
params *Params // economic and operational parameters
contract swap.Contract // reference to the smart contract
chequebookFactory swap.SimpleSwapFactory // the chequebook factory used
honeyPriceOracle HoneyOracle // oracle which resolves the price of honey (in Wei)
store state.Store // store is needed in order to keep balances and cheques across sessions
peers map[enode.ID]*Peer // map of all swap Peers
peersLock sync.RWMutex // lock for peers map
backend contract.Backend // the backend (blockchain) used
owner *Owner // contract access
params *Params // economic and operational parameters
contract contract.Contract // reference to the smart contract
chequebookFactory contract.SimpleSwapFactory // the chequebook factory used
honeyPriceOracle HoneyOracle // oracle which resolves the price of honey (in Wei)
}

// Owner encapsulates information related to accessing the contract
Expand Down Expand Up @@ -179,6 +179,7 @@ func New(dbPath string, prvkey *ecdsa.PrivateKey, backendURL string, params *Par
return nil, err
}
swapLog.Info("Using backend network ID", "ID", chainID.Uint64())

// create the owner of SWAP
owner := createOwner(prvkey)
// initialize the factory
Expand All @@ -199,6 +200,13 @@ func New(dbPath string, prvkey *ecdsa.PrivateKey, backendURL string, params *Par
if swap.contract, err = swap.StartChequebook(chequebookAddressFlag, initialDepositAmountFlag); err != nil {
return nil, err
}
availableBalance, err := swap.AvailableBalance()
if err != nil {
return nil, err
}

swapLog.Info("available balance", "balance", availableBalance)

return swap, nil
}

Expand Down Expand Up @@ -461,6 +469,37 @@ func (s *Swap) Balances() (map[enode.ID]int64, error) {
return balances, nil
}

// AvailableBalance returns the total balance of the chequebook against which new cheques can be written
func (s *Swap) AvailableBalance() (uint64, error) {
Copy link
Member

@ralph-pichler ralph-pichler Oct 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really recommend avoiding 2 blockchain wide event filters in one function, this can take quite a while on a real network (especially if using remote endpoints like infura). Instead we could compute this per peer using s.contract.availableBalanceFor(peer) + s.contract.paidOut(p.beneficiary) - p.getLastSentCheque().CumulativeAmount.

If we need this for all peers (not sure why we would though) we can compute liquidBalance() + SUM(s.contract.paidOut(peer)) - SUM(p.getLastSentCheque().CumulativeAmount). The last sum can be computed locally since we have all cheques in our store.

If we implement it that way we also don't need s.paidOut.

Copy link
Contributor Author

@Eknir Eknir Oct 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Ralph, this call is needed for a node operator to figure out how much balance is still left in his chequebook against which new cheques can be written. It allows him to figure out what his spendable balance is and when he has to re-deposit. Furthermore, this call is needed by Swarm in order to verify that an to-be-send cheque is not overspending.

I will see how and if I can incorporate your proposed formula. Thanks for the suggestion!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Ralph,
Based on your comment, I removed paidOut from the swap structure and place availableBalance there instead. This change minimizes the need to ever call the ComputeAvailableBalance function to one (boot-up).

If we would go for implementing your (second) formula instead of how it is implemented now with the event filters, we need to do a call to the smart-contract to read paidOut for every peer.

Is this indeed quicker than the event filter? Note that Infura recently updated how they handle event queries: https://blog.infura.io/faster-logs-and-events-e43e2fa13773

If you think that what you propose is indeed much quicker, I will make the update!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the option you presented and the one which is currently implemented are not entirely the same. They deviate in the case where a cheque is sent outside of Swarm and already cashed. The current implementation would recognize this (and calculate the available balance correctly from the point of cashing onwards), while your proposal would never recognize this cheque as it is not cashed by a peer, hence the availableBalance will always be off.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not consider the case with cheques outside of Swarm, this is always a user error and if done other things will break anyway. If we want the chequebook to be able to be used for other purposes we should introduce an RPC call for that so that the actual cheque creation and sending still takes place in the Swarm node.

Blockchain-wide event queries (from block 0 to "latest") can be really slow in my experience (Have you tested this call against ropsten or mainnet?). The Infura optimisation doesn't help much if it is then still slow for other users. (Since it's only being called once now it's not as big of a problem anymore).

While true that my formula does require more calls to paidOut(), this is an eth_call operation against the latest state which is a very fast operation. My per-peer formula would also have the advantage that it correctly accounts for any set hard deposit.

The current approach also doesn't update availableBalance correctly if the balance increases due to a cashed cheque after the node has started.

Copy link
Member

@ralph-pichler ralph-pichler Oct 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great answer Ralph. I am almost convinced :). Are you proposing to always do the call to availableBalance (not only during start-up), meaning we loop over the peers and do the required eth_call?

I would compute availableBalance on a per-peer basis whenever we want to send a cheque. Then it only requires two eth_call for every cheque sent.

If we want this availableBalance for all peers we have to do an eth_call for every peer but that would only happen when the RPC call is made and not during regular operation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great discussion guys!

i don't understand the issue as in-depth as you two, but at a high level, it seems sensible to me to query the source of the data (in this case the blockchain) for the latest info when it's needed.

but this, of course, is contingent upon these queries not slowing down the system significantly. it might be hard to verify this.

is the alternative (proposed by @Eknir) slower overall? or is it just less robust/more complicated? i understand the query from the genesis block to the present is slow according to @ralph-pichler, but it would be less queries in the long run, right?

Copy link
Contributor

@mortelli mortelli Oct 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need availableBalance for all peers in order to know how far away we are from needing to make a new deposit, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mortelli , it's an interesting discussion indeed. At this stage, I would personally favor robustness over efficiency, as we can always make efficiency gains later on, but robustness flaws are probably less nice to detect in a live network.
I think the alternative which I proposed was not necessarily slower, as it would only call to the blockchain upon boot-up. Subsequently, the idea was to keep a shadow view of the state of the blockchain, which requires little resources, but it is definitely less robust than calling the blockchain whenever needed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I may put my thoughts here. I pretty much have to agree with @ralph-pichler.

Blockchain-wide event queries (from block 0 to "latest") can be really slow in my experience (Have you tested this call against ropsten or mainnet?). The Infura optimisation doesn't help much if it is then still slow for other users. (Since it's only being called once now it's not as big of a problem anymore).

As @ralph-pichler says these events are horribly slow. We've been using them in Giveth and at one point it took 1.5hs to reprocess everything (around 5000 events). And this is after optimizations like querying since the contract deployment etc. Sure here it will not be that extreme in most of the cases but we should at minimum test it. And yeah chain reorgs or connection issues created a lot of errors and had to be handled.

We should not consider the case with cheques outside of Swarm, this is always a user error and if done other things will break anyway.

I also agree here. At least for the immediate future, I don't think we should be too bothered by this case. We should make an issue about this but not actively solve it now.

One of the usecases I can imagine is when user runs multiple swarm nodes with one chequebook. Still this brings a lot of problems to solve on the accounting side because it introduces all sorts of concurrency issues.

// get the LiquidBalance of the chequebook
liquidBalance, err := s.contract.LiquidBalance(nil)
if err != nil {
return 0, err
}

// get all sent Cheques
sentCheques, err := s.SentCheques()
mortelli marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, err
}

// Compute the total worth of cheques sent and how much of of this is cashed
var sentChequesWorth uint64
var cashedChequesWorth uint64
for _, ch := range sentCheques {
if ch == nil {
continue
}
sentChequesWorth += ch.ChequeParams.CumulativePayout
paidOut, err := s.contract.PaidOut(nil, ch.ChequeParams.Beneficiary)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be changed in this PR but we really need to start thinking about contexts and timeouts when doing blockchain interaction. Here we call PaidOut in a loop, each call might take a while or never terminate at all. Can you open an issue for this in case there is none?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will

Copy link
Contributor Author

@Eknir Eknir Oct 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please comment on the issue @ralph-pichler , with your recommendation for setting context. Issue: #1910

if err != nil {
return 0, err
}
cashedChequesWorth += paidOut.Uint64()
}
return liquidBalance.Uint64() + cashedChequesWorth - sentChequesWorth, nil
}

// SentCheque returns the last sent cheque for a given peer
func (s *Swap) SentCheque(peer enode.ID) (cheque *Cheque, err error) {
if swapPeer := s.getPeer(peer); swapPeer != nil {
Expand Down Expand Up @@ -594,7 +633,7 @@ func (s *Swap) Close() error {
}

// GetParams returns contract parameters (Bin, ABI, contractAddress) from the contract
func (s *Swap) GetParams() *swap.Params {
func (s *Swap) GetParams() *contract.Params {
mortelli marked this conversation as resolved.
Show resolved Hide resolved
return s.contract.ContractParams()
}

Expand Down
72 changes: 72 additions & 0 deletions swap/swap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,9 @@ func testWaitForTx(auth *bind.TransactOpts, backend cswap.Backend, tx *types.Tra
if err != nil {
return nil, err
}
if receipt.Status != types.ReceiptStatusSuccessful {
return nil, cswap.ErrTransactionReverted
}
return receipt, nil
}

Expand Down Expand Up @@ -1902,6 +1905,75 @@ func TestPeerGetLastSentCumulativePayout(t *testing.T) {
}
}

func TestAvailableBalance(t *testing.T) {
testBackend := newTestBackend()
defer testBackend.Close()
swap, clean := newTestSwap(t, ownerKey, testBackend)
defer clean()
cleanup := setupContractTest()
defer cleanup()

// deploy a chequebook
err := testDeploy(context.TODO(), swap)
if err != nil {
t.Fatal(err)
}
// create a peer
peer, err := swap.addPeer(newDummyPeerWithSpec(Spec).Peer, swap.owner.address, swap.GetParams().ContractAddress)
mortelli marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
t.Fatal(err)
}

// verify that available balance equals depositAmount (we deposit during deployment)
depositAmount := big.NewInt(9000 * int64(RetrieveRequestPrice))
availableBalance, err := swap.AvailableBalance()
if err != nil {
t.Fatal(err)
}
if availableBalance != depositAmount.Uint64() {
t.Fatalf("availableBalance not equal to deposited amount. availableBalance: %d, depositAmount: %d", availableBalance, depositAmount)
}
// withdraw 50
withdrawAmount := big.NewInt(50)
netDeposit := depositAmount.Uint64() - withdrawAmount.Uint64()
opts := bind.NewKeyedTransactor(swap.owner.privateKey)
opts.Context = context.TODO()
rec, err := swap.contract.Withdraw(opts, swap.backend, withdrawAmount)
if err != nil {
t.Fatal(err)
}
if rec.Status != types.ReceiptStatusSuccessful {
t.Fatal("Transaction reverted")
}

// verify available balance
availableBalance, err = swap.AvailableBalance()
if err != nil {
t.Fatal(err)
}
if availableBalance != netDeposit {
t.Fatalf("availableBalance not equal to deposited minus withdraw. availableBalance: %d, deposit minus withdrawn: %d", availableBalance, depositAmount.Uint64()-withdrawAmount.Uint64())
}

// send a cheque worth 42
chequeAmount := int64(42)
// create a dummy peer. Note: the peer's contract address and the peers address are resp the swap contract and the swap owner
peer.setBalance(-chequeAmount)
if err = peer.sendCheque(); err != nil {
t.Fatal(err)
}

availableBalance, err = swap.AvailableBalance()
if err != nil {
t.Fatal(err)
}
// verify available balance
if availableBalance != (netDeposit - uint64(chequeAmount)) {
t.Fatalf("availableBalance not equal to deposited minus withdraw. availableBalance: %d, deposit minus withdrawn: %d", availableBalance, depositAmount.Uint64()-withdrawAmount.Uint64())
}

}

// dummyMsgRW implements MessageReader and MessageWriter
// but doesn't do anything. Useful for dummy message sends
type dummyMsgRW struct{}
Expand Down