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

swap: chequebook persistence #1797

Merged
merged 42 commits into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7f45609
swap, contracts/swap: save used chequebook to state and attempt to l…
Eknir Sep 20, 2019
6692bae
swap: fix broken test
Eknir Sep 20, 2019
a6354de
swap: add networkID as a parameter to save/load used chequebooks from…
Eknir Sep 23, 2019
cf0ba7e
implemented NetworkID for swapTestBackend
Eknir Sep 23, 2019
56daf95
swap: added tests covering the connecting to an existing chequebook
Eknir Sep 23, 2019
6e391f0
swap: made testChequeContract2 not the same as testChequeContract
Eknir Sep 23, 2019
0c260c3
swap: saved networkID on SWAP object
Eknir Sep 23, 2019
632de27
swap: add networkID prefix to balances, received cheque and sent cheque
Eknir Sep 23, 2019
662233f
swap:renaming and comments
Eknir Sep 23, 2019
949c5dc
removed testNewSwarm:withSwap testcase because of dependency on backe…
Eknir Sep 23, 2019
5d293c6
swap: removed networkID from balances prefix
Eknir Sep 23, 2019
ac2160e
swap: more elaborate startChequebook tests, testNetworkID under testB…
Eknir Sep 24, 2019
c148730
swap: renamed networkID to backendNetworkID. Removed debug prints
Eknir Sep 24, 2019
419a06f
swap: removed redundant whitespace
Eknir Sep 24, 2019
5f00a9c
swap: fix misspell
Eknir Sep 24, 2019
7408362
swap: removed Ethereum networkID identifiers from statestore keys, ma…
Eknir Sep 25, 2019
a07c5c1
swap: error messages and comments
Eknir Sep 25, 2019
ec9260d
swap: fix tests and fix logical error with regards to storing the bac…
Eknir Sep 25, 2019
4b4e3f7
swap: updated comments on sentChequeKey and receivedChequeKey
Eknir Sep 25, 2019
6759d92
swap: remove usedBeforeAtNetworkPrefix
Eknir Sep 25, 2019
b8ff0fc
Merge branch 'master' into incentive-chequebook_persistance1
Eknir Sep 25, 2019
6b98755
Merge branch 'master' into incentive-chequebook_persistance1
Eknir Sep 25, 2019
b7f98ac
swap: prompt to startChequebook
Eknir Sep 25, 2019
26d9467
swap: initialize chequebook in swap.New
Eknir Sep 26, 2019
f9e1837
swap: make tests pass again
Eknir Sep 26, 2019
e2e8edc
swap: update comments, logs, variable names
Eknir Sep 26, 2019
e414145
swap: usedBefore fix
Eknir Sep 26, 2019
170878d
swap: updated expected error in tests
Eknir Sep 26, 2019
bbf2f9b
swap: remove auditlog from protocol_test.go
Eknir Sep 26, 2019
a5fd51a
swap: updated comment at swap.New
Eknir Sep 26, 2019
bf2cad0
swap: made comment to match godoc specification
Eknir Sep 26, 2019
2e53e83
swap: updated comment for SWAP deployment
Eknir Sep 26, 2019
675c797
swap: removed backendNetworkID from swap
Eknir Sep 26, 2019
2441747
swap: remove networkID from simulatedBackend
Eknir Sep 26, 2019
5d7484d
swap: update statestore keys
Eknir Sep 26, 2019
047422c
swap: more changes from feedback
Eknir Sep 26, 2019
98771d6
swap: assign variables in if-statement in swap-new and save chequeboo…
Eknir Sep 26, 2019
42aaca0
swap: fix reading network ID on initial boot
Eknir Sep 26, 2019
286cf36
swap: use chainID instead of networkID
Eknir Sep 26, 2019
27e2220
swap: put receivers back
Eknir Sep 26, 2019
aca25a2
swap: made test pass again
Eknir Sep 26, 2019
8b1400d
swap: change log message
Eknir Sep 26, 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
35 changes: 19 additions & 16 deletions contracts/swap/swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,6 @@ type Backend interface {
TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
}

// Deploy deploys an instance of the underlying contract and returns its `Contract` abstraction
func Deploy(auth *bind.TransactOpts, backend bind.ContractBackend, owner common.Address, harddepositTimeout time.Duration) (Contract, *types.Transaction, error) {
addr, tx, s, err := contract.DeploySimpleSwap(auth, backend, owner, big.NewInt(int64(harddepositTimeout)))
c := simpleContract{instance: s, address: addr}
return c, tx, err
}

// InstanceAt creates a new instance of a contract at a specific address.
// It assumes that there is an existing contract instance at the given address, or an error is returned
// This function is needed to communicate with remote Swap contracts (e.g. sending a cheque)
func InstanceAt(address common.Address, backend bind.ContractBackend) (Contract, error) {
simple, err := contract.NewSimpleSwap(address, backend)
c := simpleContract{instance: simple, address: address}
return c, err
}

// Contract interface defines the methods exported from the underlying go-bindings for the smart contract
type Contract interface {
// CashChequeBeneficiary cashes the cheque by the beneficiary
Expand Down Expand Up @@ -96,6 +80,25 @@ type simpleContract struct {
address common.Address
}

// Deploy deploys an instance of the underlying contract and returns its instance and the transaction identifier
func Deploy(auth *bind.TransactOpts, backend bind.ContractBackend, owner common.Address, harddepositTimeout time.Duration) (Contract, *types.Transaction, error) {
addr, tx, instance, err := contract.DeploySimpleSwap(auth, backend, owner, big.NewInt(int64(harddepositTimeout)))
c := simpleContract{instance: instance, address: addr}
return c, tx, err
}

// InstanceAt creates a new instance of a contract at a specific address.
// It assumes that there is an existing contract instance at the given address, or an error is returned
// This function is needed to communicate with remote Swap contracts (e.g. sending a cheque)
func InstanceAt(address common.Address, backend Backend) (Contract, error) {
instance, err := contract.NewSimpleSwap(address, backend)
if err != nil {
return nil, err
}
c := simpleContract{instance: instance, address: address}
return c, err
}

// CashChequeBeneficiary cashes the cheque on the blockchain and blocks until the transaction is mined.
func (s simpleContract) CashChequeBeneficiary(auth *bind.TransactOpts, backend Backend, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*CashChequeResult, *types.Receipt, error) {
tx, err := s.instance.CashChequeBeneficiary(auth, beneficiary, cumulativePayout, ownerSig)
Expand Down
241 changes: 149 additions & 92 deletions swap/swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,9 @@ type Owner struct {

// Params encapsulates param
type Params struct {
LogPath string // optional audit log path
InitialDepositAmount uint64 // initial deposit amount for the chequebook
PaymentThreshold int64 // honey amount at which a payment is triggered
DisconnectThreshold int64 // honey amount at which a peer disconnects
LogPath string // optional audit log path
PaymentThreshold int64 // honey amount at which a payment is triggered
DisconnectThreshold int64 // honey amount at which a peer disconnects
}

// newLogger returns a new logger
Expand Down Expand Up @@ -120,74 +119,97 @@ func swapRotatingFileHandler(logdir string) (log.Handler, error) {
}

// new - swap constructor without integrity check
func new(stateStore state.Store, prvkey *ecdsa.PrivateKey, backend contract.Backend, params *Params) *Swap {
auditLog = newLogger(params.LogPath)
func new(stateStore state.Store, owner *Owner, backend contract.Backend, params *Params, contract contract.Contract) *Swap {
return &Swap{
store: stateStore,
peers: make(map[enode.ID]*Peer),
backend: backend,
owner: createOwner(prvkey),
owner: owner,
params: params,
contract: contract,
honeyPriceOracle: NewHoneyPriceOracle(),
}
}

// New - swap constructor with integrity checks
func New(dbPath string, prvkey *ecdsa.PrivateKey, backendURL string, params *Params) (*Swap, error) {
// we MUST have a backend
// New prepares and creates all fields to create a swap instance:
// - sets up a SWAP database;
// - verifies whether the disconnect threshold is higher than the payment threshold;
// - connects to the blockchain backend;
// - verifies that we have not connected SWAP before on a different blockchain backend;
// - starts the chequebook; creates the swap instance
func New(dbPath string, prvkey *ecdsa.PrivateKey, backendURL string, params *Params, chequebookAddressFlag common.Address, initialDepositAmountFlag uint64) (swap *Swap, err error) {
// auditLog for swap-logging purposes
auditLog = newLogger(params.LogPath)
// verify that backendURL is not empty
if backendURL == "" {
return nil, errors.New("swap init error: no backend URL given")
return nil, errors.New("no backend URL given")
}
log.Info("connecting to SWAP API", "url", backendURL)
auditLog.Info("connecting to SWAP API", "url", backendURL)
// initialize the balances store
stateStore, err := state.NewDBStore(filepath.Join(dbPath, "swap.db"))
if err != nil {
return nil, fmt.Errorf("swap init error: %s", err)
var stateStore state.Store
if stateStore, err = state.NewDBStore(filepath.Join(dbPath, "swap.db")); err != nil {
return nil, fmt.Errorf("error while initializing statestore: %v", err)
}
if params.DisconnectThreshold <= params.PaymentThreshold {
return nil, fmt.Errorf("swap init error: disconnect threshold lower or at payment threshold. DisconnectThreshold: %d, PaymentThreshold: %d", params.DisconnectThreshold, params.PaymentThreshold)
return nil, fmt.Errorf("disconnect threshold lower or at payment threshold. DisconnectThreshold: %d, PaymentThreshold: %d", params.DisconnectThreshold, params.PaymentThreshold)
}
// connect to the backend
backend, err := ethclient.Dial(backendURL)
if err != nil {
return nil, fmt.Errorf("swap init error: error connecting to Ethereum API %s: %s", backendURL, err)
return nil, fmt.Errorf("error connecting to Ethereum API %s: %v", backendURL, err)
}

// we may not need this check, and we could maybe even get rid of this constant completely
if params != nil && params.InitialDepositAmount == 0 {
// need to prompt user for initial deposit amount
// if 0, can not cash in cheques
prompter := console.Stdin

// ask user for input
input, err := prompter.PromptInput("Please provide the amount in Wei which will deposited to your chequebook upon deployment: ")
if err != nil {
return nil, err
}
// check input
val, err := strconv.ParseInt(input, 10, 64)
if err != nil {
// maybe we should provide a fallback here? A bad input results in stopping the boot
return nil, fmt.Errorf("Conversion error while reading user input: %v", err)
}
log.Info("Chequebook initial deposit amount: ", "amount", val)
params.InitialDepositAmount = uint64(val)
// get the networkID of the backend
var networkID *big.Int
if networkID, err = backend.NetworkID(context.TODO()); err != nil {
ralph-pichler marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("error retrieving networkID from backendURL: %v", err)
}

swap := new(
// verify that we have not used SWAP before on a different networkID
if err := checkNetworkID(networkID.Uint64(), stateStore); err != nil {
return nil, err
}
auditLog.Info("SWAP initialized on backend network ID", "ID", networkID.Uint64())
// create the owner of SWAP
owner := createOwner(prvkey)
// start the chequebook
var contract contract.Contract
if contract, err = StartChequebook(chequebookAddressFlag, initialDepositAmountFlag, stateStore, owner, backend); err != nil {
ralph-pichler marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}
// create the SWAP instance
return new(
stateStore,
prvkey,
owner,
backend,
params)

return swap, nil
params,
contract,
), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

as a comment for the function in general: i think all of the network id logic is needed, but it kind of makes the code convoluted. if possible, i would extract all of this logic in a separate function.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, this whole initialization process is long and not very readable.

I also think it could be compressed. Not well verified but you could check if this works (and if it is correct):

if err == nil {
  if  usedBeforeAtNetwork != network.Uint64() {
  ...
   }
} else {
   stateStore.Put()
}
...

Maybe there is even a more elegant way.

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 put the logic in a separate function

}

const (
balancePrefix = "balance_"
sentChequePrefix = "sent_cheque_"
receivedChequePrefix = "received_cheque_"
balancePrefix = "balance_"
sentChequePrefix = "sent_cheque_"
receivedChequePrefix = "received_cheque_"
connectedChequebookKey = "connected_chequebook"
connectedBlockchainKey = "connected_blockchain"
)

// checkNetworkID verifies whether we have initialized SWAP before and ensures that we are on the same backendNetworkID if this is the case
func checkNetworkID(currentNetworkID uint64, s state.Store) (err error) {
var connectedBlockchain uint64
err = s.Get(connectedBlockchainKey, &connectedBlockchain)
// error reading from database
if err != nil && err != state.ErrNotFound {
return fmt.Errorf("error querying usedBeforeAtNetwork from statestore: %v", err)
}
// initialized before, but on a different networkID
if connectedBlockchain != currentNetworkID {
ralph-pichler marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("statestore previously used on different backend network. Used before on network: %d, Attempting to connect on network %d", connectedBlockchain, currentNetworkID)
}
// not initialized before
return s.Put(connectedBlockchainKey, currentNetworkID)

}

// returns the store key for retrieving a peer's balance
func balanceKey(peer enode.ID) string {
return balancePrefix + peer.String()
Expand Down Expand Up @@ -217,11 +239,6 @@ func createOwner(prvkey *ecdsa.PrivateKey) *Owner {
}
}

// DeploySuccess is for convenience log output
func (s *Swap) DeploySuccess() string {
return fmt.Sprintf("contract: %s, owner: %s, deposit: %v, signer: %x", s.GetParams().ContractAddress.Hex(), s.owner.address.Hex(), s.params.InitialDepositAmount, s.owner.publicKey)
}

// Add is the (sole) accounting function
// Swap implements the protocols.Balance interface
func (s *Swap) Add(amount int64, peer *protocols.Peer) (err error) {
Expand Down Expand Up @@ -457,70 +474,110 @@ func (s *Swap) getContractOwner(ctx context.Context, address common.Address) (co
return contr.Issuer(nil)
}

// StartChequebook deploys a new instance of a chequebook if chequebookAddr is empty, otherwise it wil bind to an existing instance
func (s *Swap) StartChequebook(chequebookAddr common.Address) error {
if chequebookAddr != (common.Address{}) {
if err := s.BindToContractAt(chequebookAddr); err != nil {
return err
func promptInitialDepositAmount() (uint64, error) {
// need to prompt user for initial deposit amount
// if 0, can not cash in cheques
prompter := console.Stdin

// ask user for input
input, err := prompter.PromptInput("Please provide the amount in Wei which will deposited to your chequebook upon deployment: ")
if err != nil {
return 0, err
}
// check input
val, err := strconv.ParseInt(input, 10, 64)
if err != nil {
// maybe we should provide a fallback here? A bad input results in stopping the boot
return 0, fmt.Errorf("Conversion error while reading user input: %v", err)
}
return uint64(val), nil
}

mortelli marked this conversation as resolved.
Show resolved Hide resolved
// StartChequebook starts the chequebook, taking into account the chequebookAddress passed in by the user and the chequebook addresses saved on the node's database
func StartChequebook(chequebookAddrFlag common.Address, initialDepositAmount uint64, s state.Store, owner *Owner, backend contract.Backend) (contract contract.Contract, err error) {
previouslyUsedChequebook, err := loadChequebook(s)
// error reading from disk
if err != nil && err != state.ErrNotFound {
return nil, fmt.Errorf("Error reading previously used chequebook: %s", err)
}
// read from state, but provided flag is not the same
if err == nil && (chequebookAddrFlag != common.Address{} && chequebookAddrFlag != previouslyUsedChequebook) {
return nil, fmt.Errorf("Attempting to connect to provided chequebook, but different chequebook used before")
}
// nothing written to state disk before, no flag provided: deploying new chequebook
if err == state.ErrNotFound && chequebookAddrFlag == (common.Address{}) {
var toDeposit = initialDepositAmount
if toDeposit == 0 {
toDeposit, err = promptInitialDepositAmount()
if err != nil {
return nil, err
}
}
if contract, err = Deploy(context.TODO(), toDeposit, owner, backend); err != nil {
return nil, err
}
log.Info("Using the provided chequebook", "chequebookAddr", chequebookAddr)
} else {
if err := s.Deploy(context.Background()); err != nil {
return err
if err := saveChequebook(contract.ContractParams().ContractAddress, s); err != nil {
return nil, err
}
log.Info("New SWAP contract deployed", "contract info", s.DeploySuccess())
holisticode marked this conversation as resolved.
Show resolved Hide resolved
auditLog.Info("Deployed chequebook", "contract address", contract.ContractParams().ContractAddress.Hex(), "deposit", toDeposit, "owner", owner.address)
// first time connecting by deploying a new chequebook
return contract, nil
}
return nil
// first time connecting with a chequebookAddress passed in
if chequebookAddrFlag != (common.Address{}) {
return bindToContractAt(chequebookAddrFlag, backend)
}
// reconnecting with contract read from statestore
return bindToContractAt(previouslyUsedChequebook, backend)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

same here: we have too much store/config code here and much less code actually starting the chequebook. i think it's better to separate this if possible

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 had a look and simplified where possible. If you think we can still simplify it, I propose to make an issue to tackle this


// BindToContractAt binds an instance of an already existing chequebook contract at address and sets chequebookAddr
func (s *Swap) BindToContractAt(address common.Address) (err error) {

if err := contract.ValidateCode(context.Background(), s.backend, address); err != nil {
return fmt.Errorf("contract validation for %v failed: %v", address, err)
}
s.contract, err = contract.InstanceAt(address, s.backend)
if err != nil {
return err
// BindToContractAt binds to an instance of an already existing chequebook contract at address
func bindToContractAt(address common.Address, backend contract.Backend) (contract.Contract, error) {
// validate whether address is a chequebook
if err := contract.ValidateCode(context.Background(), backend, address); err != nil {
return nil, fmt.Errorf("contract validation for %v failed: %v", address.Hex(), err)
}
return nil
auditLog.Info("bound to chequebook", "chequebookAddr", address)
// get the instance
return contract.InstanceAt(address, backend)
}

// Deploy deploys the Swap contract and sets the contract address
func (s *Swap) Deploy(ctx context.Context) error {
opts := bind.NewKeyedTransactor(s.owner.privateKey)
// Deploy deploys the Swap contract
func Deploy(ctx context.Context, initialDepositAmount uint64, owner *Owner, backend contract.Backend) (contract.Contract, error) {
opts := bind.NewKeyedTransactor(owner.privateKey)
// initial topup value
opts.Value = big.NewInt(int64(s.params.InitialDepositAmount))
opts.Value = big.NewInt(int64(initialDepositAmount))
opts.Context = ctx

auditLog.Info("deploying new swap", "owner", opts.From.Hex())
address, err := s.deployLoop(opts, s.owner.address, defaultHarddepositTimeoutDuration)
if err != nil {
auditLog.Error("unable to deploy swap", "error", err)
return err
}
auditLog.Info("swap deployed", "address", address.Hex(), "owner", opts.From.Hex())
Copy link
Contributor

Choose a reason for hiding this comment

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

why was this Info removed? can we not have this log in the deployLoop call, at least?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would like to know as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The log, when we deploy a chequebook will be:

auditLog.Info("deploying new swap", "owner", opts.From.Hex(), "deposit", opts.Value) (line 538)
auditLog.Info("Using the chequebook", "chequebookAddr", chequebookToUse) (line 512)

Given that we have these two logs, I thought it would be unnecessary to also have the log

auditLog.Info("swap deployed", "address", address.Hex(), "owner", opts.From.Hex())

If you don't agree, I will change it

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that "using the chequebook" isn't very helpful (is the chequebook address in hex?)

First "deploying new..." and then "swap deployed" makes more sense to me. It clearly shows the user that the deployment is being attempted and then that it succeeded.

Maybe we could move "using the chequebook" to a debug log.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.
We now have a log for deploying,
auditLog.Info("Deploying new swap", "owner", opts.From.Hex(), "deposit", opts.Value)
log for deployed
auditLog.Info("Deployed chequebook", "contract address", contract.ContractParams().ContractAddress.Hex(), "deposit", toDeposit, "owner", owner.address)
amd a seperate log for if we are re-using a previously deployed chequebook
auditLog.Info("bound to chequebook", "chequebookAddr", address)


return err
auditLog.Info("Deploying new swap", "owner", opts.From.Hex(), "deposit", opts.Value)
return deployLoop(opts, defaultHarddepositTimeoutDuration, owner.address, backend)
}

// deployLoop repeatedly tries to deploy the swap contract .
func (s *Swap) deployLoop(opts *bind.TransactOpts, owner common.Address, defaultHarddepositTimeoutDuration time.Duration) (addr common.Address, err error) {
func deployLoop(opts *bind.TransactOpts, defaultHarddepositTimeoutDuration time.Duration, owner common.Address, backend contract.Backend) (instance contract.Contract, err error) {
var tx *types.Transaction
for try := 0; try < deployRetries; try++ {
if try > 0 {
time.Sleep(deployDelay)
}

if s.contract, tx, err = contract.Deploy(opts, s.backend, owner, defaultHarddepositTimeoutDuration); err != nil {
if instance, tx, err = contract.Deploy(opts, backend, owner, defaultHarddepositTimeoutDuration); err != nil {
auditLog.Warn("can't send chequebook deploy tx", "try", try, "error", err)
continue
}
if addr, err = bind.WaitDeployed(opts.Context, s.backend, tx); err != nil {
if _, err := bind.WaitDeployed(opts.Context, backend, tx); err != nil {
auditLog.Warn("chequebook deploy error", "try", try, "error", err)
continue
}
return addr, nil
return instance, nil
}
return addr, err
return nil, err
}

func loadChequebook(s state.Store) (common.Address, error) {
var chequebook common.Address
err := s.Get(connectedChequebookKey, &chequebook)
return chequebook, err
}

func saveChequebook(chequebook common.Address, s state.Store) error {
return s.Put(connectedChequebookKey, chequebook)
}
Loading