123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642 |
- // 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
- }
|