// 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 --pkg contract --out contract/chequebook.go
//go:generate go run ./gencode.go
import (
"bytes"
"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/contracts/chequebook/contract"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/swarm/services/swap/swap"
"golang.org/x/net/context"
)
// TODO(zelig): watch peer solvency and notify of bouncing cheques
// TODO(zelig): enable paying with cheque by signing off
// Some functionality require 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 = big . NewInt ( 2000000 ) // gas cost of a cash transaction using chequebook
// gasToDeploy = big.NewInt(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 beneficiarys
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
}
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 ,
}
if ( contractAddr != common . Address { } ) {
self . setBalanceFromBlockChain ( )
glog . V ( logger . Detail ) . Infof ( "new chequebook initialised from %s (owner: %v, balance: %s)" , contractAddr . Hex ( ) , self . owner . Hex ( ) , self . balance . String ( ) )
}
return
}
func ( self * Chequebook ) setBalanceFromBlockChain ( ) {
balance , err := self . backend . BalanceAt ( context . TODO ( ) , self . contractAddr , nil )
if err != nil {
glog . V ( logger . Error ) . Infof ( "can't get balance: %v" , 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 ( )
}
glog . V ( logger . Detail ) . Infof ( "loaded chequebook (%s, owner: %v, balance: %v) initialised from %v" , self . contractAddr . Hex ( ) , self . owner . Hex ( ) , self . balance , 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
}
glog . V ( logger . Detail ) . Infof ( "saving chequebook (%s) to %v" , self . contractAddr . Hex ( ) , 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 . Cmp ( common . Big0 ) <= 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 {
glog . V ( logger . Warn ) . Infof ( "error depositing %d wei to chequebook (%s, balance: %v, target: %v): %v" , amount , self . contractAddr . Hex ( ) , self . balance , self . buffer , err )
return "" , err
}
// assume that transaction is actually successful, we add the amount to balance right away
self . balance . Add ( self . balance , amount )
glog . V ( logger . Detail ) . Infof ( "deposited %d wei to chequebook (%s, balance: %v, target: %v)" , amount , self . contractAddr . Hex ( ) , self . balance , 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 :
for {
select {
case <- quit :
break FOR
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 ( )
}
}
} ( )
return
}
// 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
abigen bind . ContractBackend // blockchain API
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
}
// 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 ) ,
}
glog . V ( logger . Detail ) . Infof ( "initialised inbox (%s -> %s) expected signer: %x" , self . contract . Hex ( ) , self . beneficiary . Hex ( ) , 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 )
glog . V ( logger . Detail ) . Infof ( "cashing cheque (total: %v) on chequebook (%s) sending to %v" , self . cheque . Amount , self . contract . Hex ( ) , self . beneficiary . Hex ( ) )
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 check
// 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 . Cmp ( common . Big0 ) == 0 {
return
}
ticker := time . NewTicker ( cashInterval )
self . quit = make ( chan bool )
quit := self . quit
go func ( ) {
FOR :
for {
select {
case <- quit :
break FOR
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 ( )
}
}
} ( )
return
}
// 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 check 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 ( )
}
}
glog . V ( logger . Detail ) . Infof ( "received cheque of %v wei in inbox (%s, uncashed: %v)" , amount , self . contract . Hex ( ) , 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 ) {
glog . V ( logger . Detail ) . Infof ( "verify cheque: %v - sum: %v" , self , 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 . Cmp ( common . Big0 ) <= 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
}