go-ethereum/common/chequebook/cheque.go

660 lines
21 KiB
Go

package chequebook
import (
"bytes"
"crypto/ecdsa"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"os"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/swap"
"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"
)
/*
Chequebook package is a go API to the 'chequebook' ethereum smart contract
With convenience methods that allow using chequebook for
* issuing, receiving, verifying cheques in ether
* (auto)cashing cheques in ether
* (auto)depositing ether to the chequebook contract
TODO:
* watch peer solvency and notify of bouncing cheques
* 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
Backend is the interface for that
*/
const (
gasToCash = "2000000" // gas cost of a cash transaction using chequebook
getSentAbiPre = "d75d691d" // sent amount accessor in the chequebook contract
cashAbiPre = "fbf788d6" // abi preamble signature for cash method of the chequebook
queryInterval = 15000000000 // 15 seconds
deployGas = "3000000"
// confirmationInterval = 3 * 10 * *11 // 5 minutes
)
// Backend is the interface to interact with the Ethereum blockchain
// implemented by xeth.XEth
type Backend interface {
Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error)
Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, string, error)
GetTxReceipt(txhash common.Hash) *types.Receipt
CodeAt(address string) string
}
// rlp serialised cheque model for use with the chequebook
type Cheque struct {
// the address of the contract itself needed to avoid cross-contract submission
Contract common.Address // contract address
Beneficiary common.Address // beneficiary
Amount *big.Int // cumulative amount of all funds sent
Sig []byte // signature Sign(Sha3(contract, beneficiary, amount), prvKey)
}
type Params struct {
ContractCode, ContractAbi, ContractSource string
}
var ContractParams = &Params{ContractCode, ContractAbi, ContractSource}
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)
}
// chequebook to create, sign cheques from single contract to multiple beneficiarys
// 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)
// persisted fields
balance *big.Int // not synced with blockchain
contract 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 Deploy(owner common.Address, backend Backend, amount *big.Int, confirmationInterval, timeout time.Duration) (contract common.Address, err error) {
if (owner == common.Address{}) {
return contract, fmt.Errorf("invalid owner %v. Owner needed to deploy chequebook contract: %v", owner.Hex(), err)
}
txhash, err := backend.Transact(owner.Hex(), "", "", amount.String(), deployGas, "", ContractCode)
if err != nil {
return contract, fmt.Errorf("unable to send chequebook creation transaction: %v", err)
}
timer := time.NewTimer(timeout).C
ticker := time.NewTicker(queryInterval).C
OUT:
for {
select {
case <-timer:
if ticker == nil {
// ticker is nil, receipt was found and confirmation interval passed
err = Validate(contract, backend)
if err != nil {
return contract, fmt.Errorf("invalid contract at %v after %v: %v", contract.Hex(), confirmationInterval, err)
}
break OUT
}
// ticker is non-nil meaning receipt not found yet
return contract, fmt.Errorf("chequebook deployment timed out in %v", timeout)
case <-ticker:
receipt := backend.GetTxReceipt(common.HexToHash(txhash))
if receipt != nil {
contract = receipt.ContractAddress
glog.V(logger.Detail).Infof("[CHEQUEBOOK] chequebook deployed at %v (owner: %v)", contract.Hex(), owner.Hex())
timer = time.NewTimer(confirmationInterval).C
ticker = nil
} else {
glog.V(logger.Detail).Infof("[CHEQUEBOOK] check if chequebook deployed (txhash: %v)", txhash)
}
}
}
return contract, nil
}
func Validate(contract common.Address, backend Backend) (err error) {
if (contract == common.Address{}) {
return fmt.Errorf("zero address")
}
code := backend.CodeAt(contract.Hex())
if code != ContractDeployedCode {
return fmt.Errorf("incorrect code %v:\n%v\n%v", contract.Hex(), code, ContractDeployedCode)
}
return nil
}
// NewChequebook(path, contract, balance, prvKey) creates a new Chequebook
func NewChequebook(path string, contract common.Address, prvKey *ecdsa.PrivateKey, backend Backend) (self *Chequebook, err error) {
balance := new(big.Int)
sent := make(map[common.Address]*big.Int)
owner := crypto.PubkeyToAddress(prvKey.PublicKey)
self = &Chequebook{
balance: balance,
contract: contract,
sent: sent,
path: path,
prvKey: prvKey,
backend: backend,
owner: owner,
}
if (contract != common.Address{}) {
glog.V(logger.Detail).Infof("[CHEQUEBOOK] new chequebook initialised from %v (owner: %v)", contract.Hex(), owner.Hex())
}
return
}
// LoadChequebook(path, prvKey, backend) loads a chequebook from disk (file path)
func LoadChequebook(path string, prvKey *ecdsa.PrivateKey, backend Backend) (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
}
glog.V(logger.Detail).Infof("[CHEQUEBOOK] loaded chequebook (%s, owner: %v) initialised from %v", self.contract.Hex(), self.owner.Hex(), path)
return
}
// chequebook serialisation
type chequebookFile struct {
Balance string
Contract string
Owner string
Sent map[string]string
}
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.contract = 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
}
func (self *Chequebook) MarshalJSON() ([]byte, error) {
var file = &chequebookFile{
Balance: self.balance.String(),
Contract: self.contract.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
// remembers 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("[CHEQUEBOOK] saving chequebook (%s) to %v", self.contract.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(beneficiary, amount) will create a Cheque
// the cheque is 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("insufficent 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.contract, beneficiary, sum), self.prvKey)
if err == nil {
ch = &Cheque{
Contract: self.contract,
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 issueing 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
}
// convenience method to cash any cheque
func (self *Chequebook) Cash(ch *Cheque) (txhash string, err error) {
return ch.Cash(self.owner, self.backend)
}
// 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.Sha3(input)
}
// Balance() public accessor for balance
func (self *Chequebook) Balance() *big.Int {
defer self.lock.Unlock()
self.lock.Lock()
return new(big.Int).Set(self.balance)
}
// Balance() public accessor for balance
func (self *Chequebook) Owner() common.Address {
return self.owner
}
// Backend() public accessor for backend
func (self *Chequebook) Backend() Backend {
return self.backend
}
// Address() public accessor for contract
func (self *Chequebook) Address() common.Address {
return self.contract
}
// Deposit(amount) deposits amount 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(amount) deposits amount to the chequebook account
// caller holds the lock
func (self *Chequebook) deposit(amount *big.Int) (string, error) {
txhash, err := self.backend.Transact(self.owner.Hex(), self.contract.Hex(), "", amount.String(), "", "", "")
// assume that transaction is actually successful, we add the amount to balance right away
if err != nil {
glog.V(logger.Warn).Infof("[CHEQUEBOOK] error depositing %d wei to chequebook (%s, balance: %v, target: %v): %v", amount, self.contract.Hex(), self.balance, self.buffer, err)
} else {
self.balance.Add(self.balance, amount)
glog.V(logger.Detail).Infof("[CHEQUEBOOK] deposited %d wei to chequebook (%s, balance: %v, target: %v)", amount, self.contract.Hex(), self.balance, self.buffer)
}
return txhash, err
}
// AutoDeposit(interval, threshold, buffer) (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(interval) starts a go routine that periodically sends funds to the
// chequebook contract
// caller holds the lock
// the go routine terminates if Chequebook.quit us 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
}
type Outbox struct {
chequeBook *Chequebook
beneficiary common.Address
}
func NewOutbox(chbook *Chequebook, beneficiary common.Address) *Outbox {
return &Outbox{chbook, beneficiary}
}
func (self *Outbox) Issue(amount *big.Int) (swap.Promise, error) {
return self.chequeBook.Issue(self.beneficiary, amount)
}
func (self *Outbox) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) {
self.chequeBook.AutoDeposit(interval, threshold, buffer)
}
func (self *Outbox) Stop() {}
func (self *Outbox) String() string {
return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().Hex(), self.beneficiary.Hex(), self.chequeBook.Balance())
}
// type ChequeQueue struct {
// beneficiary common.Address
// last map[string]*Inbox
// }
// inbox to deposit, verify and cash cheques
// from a single contract to single beneficiary
// 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
backend Backend // blockchain API
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(contract, beneficiary, signer, backend) constructor for Inbox
// not persisted, cumulative sum updated from blockchain when first cheque received
// backend used to sync amount (Call) as well as cash the cheques (Transact)
func NewInbox(contract, sender, beneficiary common.Address, signer *ecdsa.PublicKey, backend Backend) (self *Inbox, err error) {
if signer == nil {
return nil, fmt.Errorf("signer is null")
}
self = &Inbox{
contract: contract,
beneficiary: beneficiary,
sender: sender,
signer: signer,
backend: backend,
cashed: new(big.Int).Set(common.Big0),
}
glog.V(logger.Detail).Infof("[CHEQUEBOOK] 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 go routine to terminate
func (self *Inbox) Stop() {
defer self.lock.Unlock()
self.lock.Lock()
if self.quit != nil {
close(self.quit)
self.quit = nil
}
}
func (self *Inbox) Cash() (txhash string, err error) {
if self.cheque != nil {
txhash, err = self.cheque.Cash(self.sender, self.backend)
glog.V(logger.Detail).Infof("[CHEQUEBOOK] 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(cashInterval, maxUncashed) (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(d) starts a loop that periodically clears the last check
// if the peer is trusted, clearing period could be 24h, or a week
// caller holds the 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
}
// Reveive(cheque) called to deposit latest cheque to incoming Inbox
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
tallyhex, _, err := self.backend.Call(self.beneficiary.Hex(), self.contract.Hex(), "", "", "", getSentAbiEncode(ch.Contract))
if err != nil {
return nil, fmt.Errorf("inbox: error calling backend to set amount: %v", err)
}
tally := common.FromHex(tallyhex)
// var ok bool
// sum, ok = new(big.Int).SetString(tally, 10)
// if !ok {
// return nil, fmt.Errorf("inbox: cannot convert amount '%s' (%v) to integer", tallyhex, tally)
// }
sum = new(big.Int).SetBytes(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("[CHEQUEBOOK] received cheque of %v wei in inbox (%s, uncashed: %v)", amount, self.contract.Hex(), uncashed)
}
return amount, err
}
// RSV representation of signature
func sig2rsv(sig []byte) (v byte, r, s []byte) {
v = sig[64] + 27
r = sig[:32]
s = sig[32:64]
return
}
func getSentAbiEncode(beneficiary common.Address) string {
var beneficiary32 [32]byte
copy(beneficiary32[12:], beneficiary.Bytes())
return getSentAbiPre + common.Bytes2Hex(beneficiary32[:])
}
// abi encoding of a cheque to send as eth tx data
func (self *Cheque) cashAbiEncode() string {
v, r, s := sig2rsv(self.Sig)
// cashAbiPre, beneficiary, amount, v, r, s
bigamount := self.Amount.Bytes()
if len(bigamount) > 32 {
glog.V(logger.Detail).Infof("[CHEQUEBOOK] number too big: %v (>32 bytes)", self.Amount)
return ""
}
var beneficiary32, amount32, vabi [32]byte
copy(beneficiary32[12:], self.Beneficiary.Bytes())
copy(amount32[32-len(bigamount):32], bigamount)
vabi[31] = v
return cashAbiPre + common.Bytes2Hex(beneficiary32[:]) + common.Bytes2Hex(amount32[:]) +
common.Bytes2Hex(vabi[:]) + common.Bytes2Hex(r) + common.Bytes2Hex(s)
}
// Verify(cheque) 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("[CHEQUEBOOK] 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
}
// Cash(backend) will cash the check using xeth backend to send a transaction
// Beneficiary address should be unlocked
func (self *Cheque) Cash(sender common.Address, backend Backend) (string, error) {
return backend.Transact(sender.Hex(), self.Contract.Hex(), "", "", "", gasToCash, self.cashAbiEncode())
}