aboutsummaryrefslogblamecommitdiffstats
path: root/contracts/chequebook/cheque.go
blob: 92bd896e0a6acc8ee706cf537cf26b4c484fa0ca (plain) (tree)






















                                                                                  
                                                                                                                                                        



                                 
                 










                                                           
                                                        


                                                                       
                                             
                                                                  




                                                                  
                                                               





                                                 

                                                                                      









































                                                                                                                                                          
                                                                            



                                                                                  
 
                                                                              






























                                                                                                                                                              
                                                                



                                               
                                                                                                          






                                                                                      
                                                                              











                                                                                                                              








                                                                        
                                                              





















































                                                                                                                                           
                                                              



















                                                                                                    
                               


                                                                                       
                                                                                                                        























                                                                                                 
                                                           





























































                                                                                    
                                                                                                                                        



                                                                                                  
                                                                                                                         




























                                                                                                                                
 
                   


                                    
                                      












                                                                                             













                                                                           
                                                                  
























                                                                                                                                                              




                                                                                        
                                                                                                       


























                                                                                                                                                                  
                                                               
         
                                                                                                                                                 




















                                                                                                                                           
                                                                                                                             













                                                                                        
                                                                  
                                                                  






                                                               
                                                                                                        





                                              
 
                   


                                    
                                      











                                                                                                   



                                                                        
                                                                    






                                
                                                                                      

                                                                 
                                                                                                     
















                                                                                   
                                                                                                             






                                                                                                                              
                                                                            













                                                                                                                   
                                       








































                                                                                                                                
// Copyright 2016 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 <http://www.gnu.org/licenses/>.

// Package chequebook package wraps the 'chequebook' Ethereum smart contract.
//
// The functions in this package allow using chequebook for
// issuing, receiving, verifying cheques in ether; (auto)cashing cheques in ether
// as well as (auto)depositing ether to the chequebook contract.
package chequebook

//go:generate abigen --sol contract/chequebook.sol --exc contract/mortal.sol:mortal,contract/owned.sol:owned --pkg contract --out contract/chequebook.go
//go:generate go run ./gencode.go

import (
    "bytes"
    "context"
    "crypto/ecdsa"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "math/big"
    "os"
    "sync"
    "time"

    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "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/log"
    "github.com/ethereum/go-ethereum/swarm/services/swap/swap"
)

// TODO(zelig): watch peer solvency and notify of bouncing cheques
// TODO(zelig): enable paying with cheque by signing off

// Some functionality requires interacting with the blockchain:
// * setting current balance on peer's chequebook
// * sending the transaction to cash the cheque
// * depositing ether to the chequebook
// * watching incoming ether

var (
    gasToCash = uint64(2000000) // gas cost of a cash transaction using chequebook
    // gasToDeploy = uint64(3000000)
)

// Backend wraps all methods required for chequebook operation.
type Backend interface {
    bind.ContractBackend
    TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
    BalanceAt(ctx context.Context, address common.Address, blockNum *big.Int) (*big.Int, error)
}

// Cheque represents a payment promise to a single beneficiary.
type Cheque struct {
    Contract    common.Address // address of chequebook, needed to avoid cross-contract submission
    Beneficiary common.Address
    Amount      *big.Int // cumulative amount of all funds sent
    Sig         []byte   // signature Sign(Keccak256(contract, beneficiary, amount), prvKey)
}

func (self *Cheque) String() string {
    return fmt.Sprintf("contract: %s, beneficiary: %s, amount: %v, signature: %x", self.Contract.Hex(), self.Beneficiary.Hex(), self.Amount, self.Sig)
}

type Params struct {
    ContractCode, ContractAbi string
}

var ContractParams = &Params{contract.ChequebookBin, contract.ChequebookABI}

// Chequebook can create and sign cheques from a single contract to multiple beneficiaries.
// It is the outgoing payment handler for peer to peer micropayments.
type Chequebook struct {
    path     string                      // path to chequebook file
    prvKey   *ecdsa.PrivateKey           // private key to sign cheque with
    lock     sync.Mutex                  //
    backend  Backend                     // blockchain API
    quit     chan bool                   // when closed causes autodeposit to stop
    owner    common.Address              // owner address (derived from pubkey)
    contract *contract.Chequebook        // abigen binding
    session  *contract.ChequebookSession // abigen binding with Tx Opts

    // persisted fields
    balance      *big.Int                    // not synced with blockchain
    contractAddr common.Address              // contract address
    sent         map[common.Address]*big.Int //tallies for beneficiaries

    txhash    string   // tx hash of last deposit tx
    threshold *big.Int // threshold that triggers autodeposit if not nil
    buffer    *big.Int // buffer to keep on top of balance for fork protection

    log log.Logger // contextual logger with the contract address embedded
}

func (self *Chequebook) String() string {
    return fmt.Sprintf("contract: %s, owner: %s, balance: %v, signer: %x", self.contractAddr.Hex(), self.owner.Hex(), self.balance, self.prvKey.PublicKey)
}

// NewChequebook creates a new Chequebook.
func NewChequebook(path string, contractAddr common.Address, prvKey *ecdsa.PrivateKey, backend Backend) (self *Chequebook, err error) {
    balance := new(big.Int)
    sent := make(map[common.Address]*big.Int)

    chbook, err := contract.NewChequebook(contractAddr, backend)
    if err != nil {
        return nil, err
    }
    transactOpts := bind.NewKeyedTransactor(prvKey)
    session := &contract.ChequebookSession{
        Contract:     chbook,
        TransactOpts: *transactOpts,
    }

    self = &Chequebook{
        prvKey:       prvKey,
        balance:      balance,
        contractAddr: contractAddr,
        sent:         sent,
        path:         path,
        backend:      backend,
        owner:        transactOpts.From,
        contract:     chbook,
        session:      session,
        log:          log.New("contract", contractAddr),
    }

    if (contractAddr != common.Address{}) {
        self.setBalanceFromBlockChain()
        self.log.Trace("New chequebook initialised", "owner", self.owner, "balance", self.balance)
    }
    return
}

func (self *Chequebook) setBalanceFromBlockChain() {
    balance, err := self.backend.BalanceAt(context.TODO(), self.contractAddr, nil)
    if err != nil {
        log.Error("Failed to retrieve chequebook balance", "err", err)
    } else {
        self.balance.Set(balance)
    }
}

// LoadChequebook loads a chequebook from disk (file path).
func LoadChequebook(path string, prvKey *ecdsa.PrivateKey, backend Backend, checkBalance bool) (self *Chequebook, err error) {
    var data []byte
    data, err = ioutil.ReadFile(path)
    if err != nil {
        return
    }
    self, _ = NewChequebook(path, common.Address{}, prvKey, backend)

    err = json.Unmarshal(data, self)
    if err != nil {
        return nil, err
    }
    if checkBalance {
        self.setBalanceFromBlockChain()
    }
    log.Trace("Loaded chequebook from disk", "path", path)

    return
}

// chequebookFile is the JSON representation of a chequebook.
type chequebookFile struct {
    Balance  string
    Contract string
    Owner    string
    Sent     map[string]string
}

// UnmarshalJSON deserialises a chequebook.
func (self *Chequebook) UnmarshalJSON(data []byte) error {
    var file chequebookFile
    err := json.Unmarshal(data, &file)
    if err != nil {
        return err
    }
    _, ok := self.balance.SetString(file.Balance, 10)
    if !ok {
        return fmt.Errorf("cumulative amount sent: unable to convert string to big integer: %v", file.Balance)
    }
    self.contractAddr = common.HexToAddress(file.Contract)
    for addr, sent := range file.Sent {
        self.sent[common.HexToAddress(addr)], ok = new(big.Int).SetString(sent, 10)
        if !ok {
            return fmt.Errorf("beneficiary %v cumulative amount sent: unable to convert string to big integer: %v", addr, sent)
        }
    }
    return nil
}

// MarshalJSON serialises a chequebook.
func (self *Chequebook) MarshalJSON() ([]byte, error) {
    var file = &chequebookFile{
        Balance:  self.balance.String(),
        Contract: self.contractAddr.Hex(),
        Owner:    self.owner.Hex(),
        Sent:     make(map[string]string),
    }
    for addr, sent := range self.sent {
        file.Sent[addr.Hex()] = sent.String()
    }
    return json.Marshal(file)
}

// Save persists the chequebook on disk, remembering balance, contract address and
// cumulative amount of funds sent for each beneficiary.
func (self *Chequebook) Save() (err error) {
    data, err := json.MarshalIndent(self, "", " ")
    if err != nil {
        return err
    }
    self.log.Trace("Saving chequebook to disk", self.path)

    return ioutil.WriteFile(self.path, data, os.ModePerm)
}

// Stop quits the autodeposit go routine to terminate
func (self *Chequebook) Stop() {
    defer self.lock.Unlock()
    self.lock.Lock()
    if self.quit != nil {
        close(self.quit)
        self.quit = nil
    }
}

// Issue creates a cheque signed by the chequebook owner's private key. The
// signer commits to a contract (one that they own), a beneficiary and amount.
func (self *Chequebook) Issue(beneficiary common.Address, amount *big.Int) (ch *Cheque, err error) {
    defer self.lock.Unlock()
    self.lock.Lock()

    if amount.Sign() <= 0 {
        return nil, fmt.Errorf("amount must be greater than zero (%v)", amount)
    }
    if self.balance.Cmp(amount) < 0 {
        err = fmt.Errorf("insufficient funds to issue cheque for amount: %v. balance: %v", amount, self.balance)
    } else {
        var sig []byte
        sent, found := self.sent[beneficiary]
        if !found {
            sent = new(big.Int)
            self.sent[beneficiary] = sent
        }
        sum := new(big.Int).Set(sent)
        sum.Add(sum, amount)

        sig, err = crypto.Sign(sigHash(self.contractAddr, beneficiary, sum), self.prvKey)
        if err == nil {
            ch = &Cheque{
                Contract:    self.contractAddr,
                Beneficiary: beneficiary,
                Amount:      sum,
                Sig:         sig,
            }
            sent.Set(sum)
            self.balance.Sub(self.balance, amount) // subtract amount from balance
        }
    }

    // auto deposit if threshold is set and balance is less then threshold
    // note this is called even if issuing cheque fails
    // so we reattempt depositing
    if self.threshold != nil {
        if self.balance.Cmp(self.threshold) < 0 {
            send := new(big.Int).Sub(self.buffer, self.balance)
            self.deposit(send)
        }
    }

    return
}

// Cash is a convenience method to cash any cheque.
func (self *Chequebook) Cash(ch *Cheque) (txhash string, err error) {
    return ch.Cash(self.session)
}

// data to sign: contract address, beneficiary, cumulative amount of funds ever sent
func sigHash(contract, beneficiary common.Address, sum *big.Int) []byte {
    bigamount := sum.Bytes()
    if len(bigamount) > 32 {
        return nil
    }
    var amount32 [32]byte
    copy(amount32[32-len(bigamount):32], bigamount)
    input := append(contract.Bytes(), beneficiary.Bytes()...)
    input = append(input, amount32[:]...)
    return crypto.Keccak256(input)
}

// Balance returns the current balance of the chequebook.
func (self *Chequebook) Balance() *big.Int {
    defer self.lock.Unlock()
    self.lock.Lock()
    return new(big.Int).Set(self.balance)
}

// Owner returns the owner account of the chequebook.
func (self *Chequebook) Owner() common.Address {
    return self.owner
}

// Address returns the on-chain contract address of the chequebook.
func (self *Chequebook) Address() common.Address {
    return self.contractAddr
}

// Deposit deposits money to the chequebook account.
func (self *Chequebook) Deposit(amount *big.Int) (string, error) {
    defer self.lock.Unlock()
    self.lock.Lock()
    return self.deposit(amount)
}

// deposit deposits amount to the chequebook account.
// The caller must hold self.lock.
func (self *Chequebook) deposit(amount *big.Int) (string, error) {
    // since the amount is variable here, we do not use sessions
    depositTransactor := bind.NewKeyedTransactor(self.prvKey)
    depositTransactor.Value = amount
    chbookRaw := &contract.ChequebookRaw{Contract: self.contract}
    tx, err := chbookRaw.Transfer(depositTransactor)
    if err != nil {
        self.log.Warn("Failed to fund chequebook", "amount", amount, "balance", self.balance, "target", self.buffer, "err", err)
        return "", err
    }
    // assume that transaction is actually successful, we add the amount to balance right away
    self.balance.Add(self.balance, amount)
    self.log.Trace("Deposited funds to chequebook", "amount", amount, "balance", self.balance, "target", self.buffer)
    return tx.Hash().Hex(), nil
}

// AutoDeposit (re)sets interval time and amount which triggers sending funds to the
// chequebook. Contract backend needs to be set if threshold is not less than buffer, then
// deposit will be triggered on every new cheque issued.
func (self *Chequebook) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) {
    defer self.lock.Unlock()
    self.lock.Lock()
    self.threshold = threshold
    self.buffer = buffer
    self.autoDeposit(interval)
}

// autoDeposit starts a goroutine that periodically sends funds to the chequebook
// contract caller holds the lock the go routine terminates if Chequebook.quit is closed.
func (self *Chequebook) autoDeposit(interval time.Duration) {
    if self.quit != nil {
        close(self.quit)
        self.quit = nil
    }
    // if threshold >= balance autodeposit after every cheque issued
    if interval == time.Duration(0) || self.threshold != nil && self.buffer != nil && self.threshold.Cmp(self.buffer) >= 0 {
        return
    }

    ticker := time.NewTicker(interval)
    self.quit = make(chan bool)
    quit := self.quit

    go func() {
        for {
            select {
            case <-quit:
                return
            case <-ticker.C:
                self.lock.Lock()
                if self.balance.Cmp(self.buffer) < 0 {
                    amount := new(big.Int).Sub(self.buffer, self.balance)
                    txhash, err := self.deposit(amount)
                    if err == nil {
                        self.txhash = txhash
                    }
                }
                self.lock.Unlock()
            }
        }
    }()
}

// Outbox can issue cheques from a single contract to a single beneficiary.
type Outbox struct {
    chequeBook  *Chequebook
    beneficiary common.Address
}

// NewOutbox creates an outbox.
func NewOutbox(chbook *Chequebook, beneficiary common.Address) *Outbox {
    return &Outbox{chbook, beneficiary}
}

// Issue creates cheque.
func (self *Outbox) Issue(amount *big.Int) (swap.Promise, error) {
    return self.chequeBook.Issue(self.beneficiary, amount)
}

// AutoDeposit enables auto-deposits on the underlying chequebook.
func (self *Outbox) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) {
    self.chequeBook.AutoDeposit(interval, threshold, buffer)
}

// Stop helps satisfy the swap.OutPayment interface.
func (self *Outbox) Stop() {}

// String implements fmt.Stringer.
func (self *Outbox) String() string {
    return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().Hex(), self.beneficiary.Hex(), self.chequeBook.Balance())
}

// Inbox can deposit, verify and cash cheques from a single contract to a single
// beneficiary. It is the incoming payment handler for peer to peer micropayments.
type Inbox struct {
    lock        sync.Mutex
    contract    common.Address              // peer's chequebook contract
    beneficiary common.Address              // local peer's receiving address
    sender      common.Address              // local peer's address to send cashing tx from
    signer      *ecdsa.PublicKey            // peer's public key
    txhash      string                      // tx hash of last cashing tx
    session     *contract.ChequebookSession // abi contract backend with tx opts
    quit        chan bool                   // when closed causes autocash to stop
    maxUncashed *big.Int                    // threshold that triggers autocashing
    cashed      *big.Int                    // cumulative amount cashed
    cheque      *Cheque                     // last cheque, nil if none yet received
    log         log.Logger                  // contextual logger with the contract address embedded
}

// NewInbox creates an Inbox. An Inboxes is not persisted, the cumulative sum is updated
// from blockchain when first cheque is received.
func NewInbox(prvKey *ecdsa.PrivateKey, contractAddr, beneficiary common.Address, signer *ecdsa.PublicKey, abigen bind.ContractBackend) (self *Inbox, err error) {
    if signer == nil {
        return nil, fmt.Errorf("signer is null")
    }
    chbook, err := contract.NewChequebook(contractAddr, abigen)
    if err != nil {
        return nil, err
    }
    transactOpts := bind.NewKeyedTransactor(prvKey)
    transactOpts.GasLimit = gasToCash
    session := &contract.ChequebookSession{
        Contract:     chbook,
        TransactOpts: *transactOpts,
    }
    sender := transactOpts.From

    self = &Inbox{
        contract:    contractAddr,
        beneficiary: beneficiary,
        sender:      sender,
        signer:      signer,
        session:     session,
        cashed:      new(big.Int).Set(common.Big0),
        log:         log.New("contract", contractAddr),
    }
    self.log.Trace("New chequebook inbox initialized", "beneficiary", self.beneficiary, "signer", hexutil.Bytes(crypto.FromECDSAPub(signer)))
    return
}

func (self *Inbox) String() string {
    return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.contract.Hex(), self.beneficiary.Hex(), self.cheque.Amount)
}

// Stop quits the autocash goroutine.
func (self *Inbox) Stop() {
    defer self.lock.Unlock()
    self.lock.Lock()
    if self.quit != nil {
        close(self.quit)
        self.quit = nil
    }
}

// Cash attempts to cash the current cheque.
func (self *Inbox) Cash() (txhash string, err error) {
    if self.cheque != nil {
        txhash, err = self.cheque.Cash(self.session)
        self.log.Trace("Cashing in chequebook cheque", "amount", self.cheque.Amount, "beneficiary", self.beneficiary)
        self.cashed = self.cheque.Amount
    }
    return
}

// AutoCash (re)sets maximum time and amount which triggers cashing of the last uncashed
// cheque if maxUncashed is set to 0, then autocash on receipt.
func (self *Inbox) AutoCash(cashInterval time.Duration, maxUncashed *big.Int) {
    defer self.lock.Unlock()
    self.lock.Lock()
    self.maxUncashed = maxUncashed
    self.autoCash(cashInterval)
}

// autoCash starts a loop that periodically clears the last cheque
// if the peer is trusted. Clearing period could be 24h or a week.
// The caller must hold self.lock.
func (self *Inbox) autoCash(cashInterval time.Duration) {
    if self.quit != nil {
        close(self.quit)
        self.quit = nil
    }
    // if maxUncashed is set to 0, then autocash on receipt
    if cashInterval == time.Duration(0) || self.maxUncashed != nil && self.maxUncashed.Sign() == 0 {
        return
    }

    ticker := time.NewTicker(cashInterval)
    self.quit = make(chan bool)
    quit := self.quit

    go func() {
        for {
            select {
            case <-quit:
                return
            case <-ticker.C:
                self.lock.Lock()
                if self.cheque != nil && self.cheque.Amount.Cmp(self.cashed) != 0 {
                    txhash, err := self.Cash()
                    if err == nil {
                        self.txhash = txhash
                    }
                }
                self.lock.Unlock()
            }
        }
    }()
}

// Receive is called to deposit the latest cheque to the incoming Inbox.
// The given promise must be a *Cheque.
func (self *Inbox) Receive(promise swap.Promise) (*big.Int, error) {
    ch := promise.(*Cheque)

    defer self.lock.Unlock()
    self.lock.Lock()

    var sum *big.Int
    if self.cheque == nil {
        // the sum is checked against the blockchain once a cheque is received
        tally, err := self.session.Sent(self.beneficiary)
        if err != nil {
            return nil, fmt.Errorf("inbox: error calling backend to set amount: %v", err)
        }
        sum = tally
    } else {
        sum = self.cheque.Amount
    }

    amount, err := ch.Verify(self.signer, self.contract, self.beneficiary, sum)
    var uncashed *big.Int
    if err == nil {
        self.cheque = ch

        if self.maxUncashed != nil {
            uncashed = new(big.Int).Sub(ch.Amount, self.cashed)
            if self.maxUncashed.Cmp(uncashed) < 0 {
                self.Cash()
            }
        }
        self.log.Trace("Received cheque in chequebook inbox", "amount", amount, "uncashed", uncashed)
    }

    return amount, err
}

// Verify verifies cheque for signer, contract, beneficiary, amount, valid signature.
func (self *Cheque) Verify(signerKey *ecdsa.PublicKey, contract, beneficiary common.Address, sum *big.Int) (*big.Int, error) {
    log.Trace("Verifying chequebook cheque", "cheque", self, "sum", sum)
    if sum == nil {
        return nil, fmt.Errorf("invalid amount")
    }

    if self.Beneficiary != beneficiary {
        return nil, fmt.Errorf("beneficiary mismatch: %v != %v", self.Beneficiary.Hex(), beneficiary.Hex())
    }
    if self.Contract != contract {
        return nil, fmt.Errorf("contract mismatch: %v != %v", self.Contract.Hex(), contract.Hex())
    }

    amount := new(big.Int).Set(self.Amount)
    if sum != nil {
        amount.Sub(amount, sum)
        if amount.Sign() <= 0 {
            return nil, fmt.Errorf("incorrect amount: %v <= 0", amount)
        }
    }

    pubKey, err := crypto.SigToPub(sigHash(self.Contract, beneficiary, self.Amount), self.Sig)
    if err != nil {
        return nil, fmt.Errorf("invalid signature: %v", err)
    }
    if !bytes.Equal(crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) {
        return nil, fmt.Errorf("signer mismatch: %x != %x", crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey))
    }
    return amount, nil
}

// v/r/s representation of signature
func sig2vrs(sig []byte) (v byte, r, s [32]byte) {
    v = sig[64] + 27
    copy(r[:], sig[:32])
    copy(s[:], sig[32:64])
    return
}

// Cash cashes the cheque by sending an Ethereum transaction.
func (self *Cheque) Cash(session *contract.ChequebookSession) (string, error) {
    v, r, s := sig2vrs(self.Sig)
    tx, err := session.Cash(self.Beneficiary, self.Amount, v, r, s)
    if err != nil {
        return "", err
    }
    return tx.Hash().Hex(), nil
}

// ValidateCode checks that the on-chain code at address matches the expected chequebook
// contract code. This is used to detect suicided chequebooks.
func ValidateCode(ctx context.Context, b Backend, address common.Address) (ok bool, err error) {
    code, err := b.CodeAt(ctx, address, nil)
    if err != nil {
        return false, err
    }
    return bytes.Equal(code, common.FromHex(contract.ContractDeployedCode)), nil
}