github.com/bamzi/go-ethereum@v1.6.7-0.20170704111104-138f26c93af1/contracts/chequebook/cheque.go (about) 1 // Copyright 2016 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 // Package chequebook package wraps the 'chequebook' Ethereum smart contract. 18 // 19 // The functions in this package allow using chequebook for 20 // issuing, receiving, verifying cheques in ether; (auto)cashing cheques in ether 21 // as well as (auto)depositing ether to the chequebook contract. 22 package chequebook 23 24 //go:generate abigen --sol contract/chequebook.sol --pkg contract --out contract/chequebook.go 25 //go:generate go run ./gencode.go 26 27 import ( 28 "bytes" 29 "context" 30 "crypto/ecdsa" 31 "encoding/json" 32 "fmt" 33 "io/ioutil" 34 "math/big" 35 "os" 36 "sync" 37 "time" 38 39 "github.com/ethereum/go-ethereum/accounts/abi/bind" 40 "github.com/ethereum/go-ethereum/common" 41 "github.com/ethereum/go-ethereum/common/hexutil" 42 "github.com/ethereum/go-ethereum/contracts/chequebook/contract" 43 "github.com/ethereum/go-ethereum/core/types" 44 "github.com/ethereum/go-ethereum/crypto" 45 "github.com/ethereum/go-ethereum/log" 46 "github.com/ethereum/go-ethereum/swarm/services/swap/swap" 47 ) 48 49 // TODO(zelig): watch peer solvency and notify of bouncing cheques 50 // TODO(zelig): enable paying with cheque by signing off 51 52 // Some functionality requires interacting with the blockchain: 53 // * setting current balance on peer's chequebook 54 // * sending the transaction to cash the cheque 55 // * depositing ether to the chequebook 56 // * watching incoming ether 57 58 var ( 59 gasToCash = big.NewInt(2000000) // gas cost of a cash transaction using chequebook 60 // gasToDeploy = big.NewInt(3000000) 61 ) 62 63 // Backend wraps all methods required for chequebook operation. 64 type Backend interface { 65 bind.ContractBackend 66 TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) 67 BalanceAt(ctx context.Context, address common.Address, blockNum *big.Int) (*big.Int, error) 68 } 69 70 // Cheque represents a payment promise to a single beneficiary. 71 type Cheque struct { 72 Contract common.Address // address of chequebook, needed to avoid cross-contract submission 73 Beneficiary common.Address 74 Amount *big.Int // cumulative amount of all funds sent 75 Sig []byte // signature Sign(Keccak256(contract, beneficiary, amount), prvKey) 76 } 77 78 func (self *Cheque) String() string { 79 return fmt.Sprintf("contract: %s, beneficiary: %s, amount: %v, signature: %x", self.Contract.Hex(), self.Beneficiary.Hex(), self.Amount, self.Sig) 80 } 81 82 type Params struct { 83 ContractCode, ContractAbi string 84 } 85 86 var ContractParams = &Params{contract.ChequebookBin, contract.ChequebookABI} 87 88 // Chequebook can create and sign cheques from a single contract to multiple beneficiaries. 89 // It is the outgoing payment handler for peer to peer micropayments. 90 type Chequebook struct { 91 path string // path to chequebook file 92 prvKey *ecdsa.PrivateKey // private key to sign cheque with 93 lock sync.Mutex // 94 backend Backend // blockchain API 95 quit chan bool // when closed causes autodeposit to stop 96 owner common.Address // owner address (derived from pubkey) 97 contract *contract.Chequebook // abigen binding 98 session *contract.ChequebookSession // abigen binding with Tx Opts 99 100 // persisted fields 101 balance *big.Int // not synced with blockchain 102 contractAddr common.Address // contract address 103 sent map[common.Address]*big.Int //tallies for beneficiaries 104 105 txhash string // tx hash of last deposit tx 106 threshold *big.Int // threshold that triggers autodeposit if not nil 107 buffer *big.Int // buffer to keep on top of balance for fork protection 108 109 log log.Logger // contextual logger with the contract address embedded 110 } 111 112 func (self *Chequebook) String() string { 113 return fmt.Sprintf("contract: %s, owner: %s, balance: %v, signer: %x", self.contractAddr.Hex(), self.owner.Hex(), self.balance, self.prvKey.PublicKey) 114 } 115 116 // NewChequebook creates a new Chequebook. 117 func NewChequebook(path string, contractAddr common.Address, prvKey *ecdsa.PrivateKey, backend Backend) (self *Chequebook, err error) { 118 balance := new(big.Int) 119 sent := make(map[common.Address]*big.Int) 120 121 chbook, err := contract.NewChequebook(contractAddr, backend) 122 if err != nil { 123 return nil, err 124 } 125 transactOpts := bind.NewKeyedTransactor(prvKey) 126 session := &contract.ChequebookSession{ 127 Contract: chbook, 128 TransactOpts: *transactOpts, 129 } 130 131 self = &Chequebook{ 132 prvKey: prvKey, 133 balance: balance, 134 contractAddr: contractAddr, 135 sent: sent, 136 path: path, 137 backend: backend, 138 owner: transactOpts.From, 139 contract: chbook, 140 session: session, 141 log: log.New("contract", contractAddr), 142 } 143 144 if (contractAddr != common.Address{}) { 145 self.setBalanceFromBlockChain() 146 self.log.Trace("New chequebook initialised", "owner", self.owner, "balance", self.balance) 147 } 148 return 149 } 150 151 func (self *Chequebook) setBalanceFromBlockChain() { 152 balance, err := self.backend.BalanceAt(context.TODO(), self.contractAddr, nil) 153 if err != nil { 154 log.Error("Failed to retrieve chequebook balance", "err", err) 155 } else { 156 self.balance.Set(balance) 157 } 158 } 159 160 // LoadChequebook loads a chequebook from disk (file path). 161 func LoadChequebook(path string, prvKey *ecdsa.PrivateKey, backend Backend, checkBalance bool) (self *Chequebook, err error) { 162 var data []byte 163 data, err = ioutil.ReadFile(path) 164 if err != nil { 165 return 166 } 167 self, _ = NewChequebook(path, common.Address{}, prvKey, backend) 168 169 err = json.Unmarshal(data, self) 170 if err != nil { 171 return nil, err 172 } 173 if checkBalance { 174 self.setBalanceFromBlockChain() 175 } 176 log.Trace("Loaded chequebook from disk", "path", path) 177 178 return 179 } 180 181 // chequebookFile is the JSON representation of a chequebook. 182 type chequebookFile struct { 183 Balance string 184 Contract string 185 Owner string 186 Sent map[string]string 187 } 188 189 // UnmarshalJSON deserialises a chequebook. 190 func (self *Chequebook) UnmarshalJSON(data []byte) error { 191 var file chequebookFile 192 err := json.Unmarshal(data, &file) 193 if err != nil { 194 return err 195 } 196 _, ok := self.balance.SetString(file.Balance, 10) 197 if !ok { 198 return fmt.Errorf("cumulative amount sent: unable to convert string to big integer: %v", file.Balance) 199 } 200 self.contractAddr = common.HexToAddress(file.Contract) 201 for addr, sent := range file.Sent { 202 self.sent[common.HexToAddress(addr)], ok = new(big.Int).SetString(sent, 10) 203 if !ok { 204 return fmt.Errorf("beneficiary %v cumulative amount sent: unable to convert string to big integer: %v", addr, sent) 205 } 206 } 207 return nil 208 } 209 210 // MarshalJSON serialises a chequebook. 211 func (self *Chequebook) MarshalJSON() ([]byte, error) { 212 var file = &chequebookFile{ 213 Balance: self.balance.String(), 214 Contract: self.contractAddr.Hex(), 215 Owner: self.owner.Hex(), 216 Sent: make(map[string]string), 217 } 218 for addr, sent := range self.sent { 219 file.Sent[addr.Hex()] = sent.String() 220 } 221 return json.Marshal(file) 222 } 223 224 // Save persists the chequebook on disk, remembering balance, contract address and 225 // cumulative amount of funds sent for each beneficiary. 226 func (self *Chequebook) Save() (err error) { 227 data, err := json.MarshalIndent(self, "", " ") 228 if err != nil { 229 return err 230 } 231 self.log.Trace("Saving chequebook to disk", self.path) 232 233 return ioutil.WriteFile(self.path, data, os.ModePerm) 234 } 235 236 // Stop quits the autodeposit go routine to terminate 237 func (self *Chequebook) Stop() { 238 defer self.lock.Unlock() 239 self.lock.Lock() 240 if self.quit != nil { 241 close(self.quit) 242 self.quit = nil 243 } 244 } 245 246 // Issue creates a cheque signed by the chequebook owner's private key. The 247 // signer commits to a contract (one that they own), a beneficiary and amount. 248 func (self *Chequebook) Issue(beneficiary common.Address, amount *big.Int) (ch *Cheque, err error) { 249 defer self.lock.Unlock() 250 self.lock.Lock() 251 252 if amount.Sign() <= 0 { 253 return nil, fmt.Errorf("amount must be greater than zero (%v)", amount) 254 } 255 if self.balance.Cmp(amount) < 0 { 256 err = fmt.Errorf("insufficient funds to issue cheque for amount: %v. balance: %v", amount, self.balance) 257 } else { 258 var sig []byte 259 sent, found := self.sent[beneficiary] 260 if !found { 261 sent = new(big.Int) 262 self.sent[beneficiary] = sent 263 } 264 sum := new(big.Int).Set(sent) 265 sum.Add(sum, amount) 266 267 sig, err = crypto.Sign(sigHash(self.contractAddr, beneficiary, sum), self.prvKey) 268 if err == nil { 269 ch = &Cheque{ 270 Contract: self.contractAddr, 271 Beneficiary: beneficiary, 272 Amount: sum, 273 Sig: sig, 274 } 275 sent.Set(sum) 276 self.balance.Sub(self.balance, amount) // subtract amount from balance 277 } 278 } 279 280 // auto deposit if threshold is set and balance is less then threshold 281 // note this is called even if issuing cheque fails 282 // so we reattempt depositing 283 if self.threshold != nil { 284 if self.balance.Cmp(self.threshold) < 0 { 285 send := new(big.Int).Sub(self.buffer, self.balance) 286 self.deposit(send) 287 } 288 } 289 290 return 291 } 292 293 // Cash is a convenience method to cash any cheque. 294 func (self *Chequebook) Cash(ch *Cheque) (txhash string, err error) { 295 return ch.Cash(self.session) 296 } 297 298 // data to sign: contract address, beneficiary, cumulative amount of funds ever sent 299 func sigHash(contract, beneficiary common.Address, sum *big.Int) []byte { 300 bigamount := sum.Bytes() 301 if len(bigamount) > 32 { 302 return nil 303 } 304 var amount32 [32]byte 305 copy(amount32[32-len(bigamount):32], bigamount) 306 input := append(contract.Bytes(), beneficiary.Bytes()...) 307 input = append(input, amount32[:]...) 308 return crypto.Keccak256(input) 309 } 310 311 // Balance returns the current balance of the chequebook. 312 func (self *Chequebook) Balance() *big.Int { 313 defer self.lock.Unlock() 314 self.lock.Lock() 315 return new(big.Int).Set(self.balance) 316 } 317 318 // Owner returns the owner account of the chequebook. 319 func (self *Chequebook) Owner() common.Address { 320 return self.owner 321 } 322 323 // Address returns the on-chain contract address of the chequebook. 324 func (self *Chequebook) Address() common.Address { 325 return self.contractAddr 326 } 327 328 // Deposit deposits money to the chequebook account. 329 func (self *Chequebook) Deposit(amount *big.Int) (string, error) { 330 defer self.lock.Unlock() 331 self.lock.Lock() 332 return self.deposit(amount) 333 } 334 335 // deposit deposits amount to the chequebook account. 336 // The caller must hold self.lock. 337 func (self *Chequebook) deposit(amount *big.Int) (string, error) { 338 // since the amount is variable here, we do not use sessions 339 depositTransactor := bind.NewKeyedTransactor(self.prvKey) 340 depositTransactor.Value = amount 341 chbookRaw := &contract.ChequebookRaw{Contract: self.contract} 342 tx, err := chbookRaw.Transfer(depositTransactor) 343 if err != nil { 344 self.log.Warn("Failed to fund chequebook", "amount", amount, "balance", self.balance, "target", self.buffer, "err", err) 345 return "", err 346 } 347 // assume that transaction is actually successful, we add the amount to balance right away 348 self.balance.Add(self.balance, amount) 349 self.log.Trace("Deposited funds to chequebook", "amount", amount, "balance", self.balance, "target", self.buffer) 350 return tx.Hash().Hex(), nil 351 } 352 353 // AutoDeposit (re)sets interval time and amount which triggers sending funds to the 354 // chequebook. Contract backend needs to be set if threshold is not less than buffer, then 355 // deposit will be triggered on every new cheque issued. 356 func (self *Chequebook) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { 357 defer self.lock.Unlock() 358 self.lock.Lock() 359 self.threshold = threshold 360 self.buffer = buffer 361 self.autoDeposit(interval) 362 } 363 364 // autoDeposit starts a goroutine that periodically sends funds to the chequebook 365 // contract caller holds the lock the go routine terminates if Chequebook.quit is closed. 366 func (self *Chequebook) autoDeposit(interval time.Duration) { 367 if self.quit != nil { 368 close(self.quit) 369 self.quit = nil 370 } 371 // if threshold >= balance autodeposit after every cheque issued 372 if interval == time.Duration(0) || self.threshold != nil && self.buffer != nil && self.threshold.Cmp(self.buffer) >= 0 { 373 return 374 } 375 376 ticker := time.NewTicker(interval) 377 self.quit = make(chan bool) 378 quit := self.quit 379 go func() { 380 FOR: 381 for { 382 select { 383 case <-quit: 384 break FOR 385 case <-ticker.C: 386 self.lock.Lock() 387 if self.balance.Cmp(self.buffer) < 0 { 388 amount := new(big.Int).Sub(self.buffer, self.balance) 389 txhash, err := self.deposit(amount) 390 if err == nil { 391 self.txhash = txhash 392 } 393 } 394 self.lock.Unlock() 395 } 396 } 397 }() 398 return 399 } 400 401 // Outbox can issue cheques from a single contract to a single beneficiary. 402 type Outbox struct { 403 chequeBook *Chequebook 404 beneficiary common.Address 405 } 406 407 // NewOutbox creates an outbox. 408 func NewOutbox(chbook *Chequebook, beneficiary common.Address) *Outbox { 409 return &Outbox{chbook, beneficiary} 410 } 411 412 // Issue creates cheque. 413 func (self *Outbox) Issue(amount *big.Int) (swap.Promise, error) { 414 return self.chequeBook.Issue(self.beneficiary, amount) 415 } 416 417 // AutoDeposit enables auto-deposits on the underlying chequebook. 418 func (self *Outbox) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { 419 self.chequeBook.AutoDeposit(interval, threshold, buffer) 420 } 421 422 // Stop helps satisfy the swap.OutPayment interface. 423 func (self *Outbox) Stop() {} 424 425 // String implements fmt.Stringer. 426 func (self *Outbox) String() string { 427 return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().Hex(), self.beneficiary.Hex(), self.chequeBook.Balance()) 428 } 429 430 // Inbox can deposit, verify and cash cheques from a single contract to a single 431 // beneficiary. It is the incoming payment handler for peer to peer micropayments. 432 type Inbox struct { 433 lock sync.Mutex 434 contract common.Address // peer's chequebook contract 435 beneficiary common.Address // local peer's receiving address 436 sender common.Address // local peer's address to send cashing tx from 437 signer *ecdsa.PublicKey // peer's public key 438 txhash string // tx hash of last cashing tx 439 abigen bind.ContractBackend // blockchain API 440 session *contract.ChequebookSession // abi contract backend with tx opts 441 quit chan bool // when closed causes autocash to stop 442 maxUncashed *big.Int // threshold that triggers autocashing 443 cashed *big.Int // cumulative amount cashed 444 cheque *Cheque // last cheque, nil if none yet received 445 log log.Logger // contextual logger with the contract address embedded 446 } 447 448 // NewInbox creates an Inbox. An Inboxes is not persisted, the cumulative sum is updated 449 // from blockchain when first cheque is received. 450 func NewInbox(prvKey *ecdsa.PrivateKey, contractAddr, beneficiary common.Address, signer *ecdsa.PublicKey, abigen bind.ContractBackend) (self *Inbox, err error) { 451 if signer == nil { 452 return nil, fmt.Errorf("signer is null") 453 } 454 chbook, err := contract.NewChequebook(contractAddr, abigen) 455 if err != nil { 456 return nil, err 457 } 458 transactOpts := bind.NewKeyedTransactor(prvKey) 459 transactOpts.GasLimit = gasToCash 460 session := &contract.ChequebookSession{ 461 Contract: chbook, 462 TransactOpts: *transactOpts, 463 } 464 sender := transactOpts.From 465 466 self = &Inbox{ 467 contract: contractAddr, 468 beneficiary: beneficiary, 469 sender: sender, 470 signer: signer, 471 session: session, 472 cashed: new(big.Int).Set(common.Big0), 473 log: log.New("contract", contractAddr), 474 } 475 self.log.Trace("New chequebook inbox initialized", "beneficiary", self.beneficiary, "signer", hexutil.Bytes(crypto.FromECDSAPub(signer))) 476 return 477 } 478 479 func (self *Inbox) String() string { 480 return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.contract.Hex(), self.beneficiary.Hex(), self.cheque.Amount) 481 } 482 483 // Stop quits the autocash goroutine. 484 func (self *Inbox) Stop() { 485 defer self.lock.Unlock() 486 self.lock.Lock() 487 if self.quit != nil { 488 close(self.quit) 489 self.quit = nil 490 } 491 } 492 493 // Cash attempts to cash the current cheque. 494 func (self *Inbox) Cash() (txhash string, err error) { 495 if self.cheque != nil { 496 txhash, err = self.cheque.Cash(self.session) 497 self.log.Trace("Cashing in chequebook cheque", "amount", self.cheque.Amount, "beneficiary", self.beneficiary) 498 self.cashed = self.cheque.Amount 499 } 500 return 501 } 502 503 // AutoCash (re)sets maximum time and amount which triggers cashing of the last uncashed 504 // cheque if maxUncashed is set to 0, then autocash on receipt. 505 func (self *Inbox) AutoCash(cashInterval time.Duration, maxUncashed *big.Int) { 506 defer self.lock.Unlock() 507 self.lock.Lock() 508 self.maxUncashed = maxUncashed 509 self.autoCash(cashInterval) 510 } 511 512 // autoCash starts a loop that periodically clears the last cheque 513 // if the peer is trusted. Clearing period could be 24h or a week. 514 // The caller must hold self.lock. 515 func (self *Inbox) autoCash(cashInterval time.Duration) { 516 if self.quit != nil { 517 close(self.quit) 518 self.quit = nil 519 } 520 // if maxUncashed is set to 0, then autocash on receipt 521 if cashInterval == time.Duration(0) || self.maxUncashed != nil && self.maxUncashed.Sign() == 0 { 522 return 523 } 524 525 ticker := time.NewTicker(cashInterval) 526 self.quit = make(chan bool) 527 quit := self.quit 528 go func() { 529 FOR: 530 for { 531 select { 532 case <-quit: 533 break FOR 534 case <-ticker.C: 535 self.lock.Lock() 536 if self.cheque != nil && self.cheque.Amount.Cmp(self.cashed) != 0 { 537 txhash, err := self.Cash() 538 if err == nil { 539 self.txhash = txhash 540 } 541 } 542 self.lock.Unlock() 543 } 544 } 545 }() 546 return 547 } 548 549 // Receive is called to deposit the latest cheque to the incoming Inbox. 550 // The given promise must be a *Cheque. 551 func (self *Inbox) Receive(promise swap.Promise) (*big.Int, error) { 552 ch := promise.(*Cheque) 553 554 defer self.lock.Unlock() 555 self.lock.Lock() 556 557 var sum *big.Int 558 if self.cheque == nil { 559 // the sum is checked against the blockchain once a cheque is received 560 tally, err := self.session.Sent(self.beneficiary) 561 if err != nil { 562 return nil, fmt.Errorf("inbox: error calling backend to set amount: %v", err) 563 } 564 sum = tally 565 } else { 566 sum = self.cheque.Amount 567 } 568 569 amount, err := ch.Verify(self.signer, self.contract, self.beneficiary, sum) 570 var uncashed *big.Int 571 if err == nil { 572 self.cheque = ch 573 574 if self.maxUncashed != nil { 575 uncashed = new(big.Int).Sub(ch.Amount, self.cashed) 576 if self.maxUncashed.Cmp(uncashed) < 0 { 577 self.Cash() 578 } 579 } 580 self.log.Trace("Received cheque in chequebook inbox", "amount", amount, "uncashed", uncashed) 581 } 582 583 return amount, err 584 } 585 586 // Verify verifies cheque for signer, contract, beneficiary, amount, valid signature. 587 func (self *Cheque) Verify(signerKey *ecdsa.PublicKey, contract, beneficiary common.Address, sum *big.Int) (*big.Int, error) { 588 log.Trace("Verifying chequebook cheque", "cheque", self, "sum", sum) 589 if sum == nil { 590 return nil, fmt.Errorf("invalid amount") 591 } 592 593 if self.Beneficiary != beneficiary { 594 return nil, fmt.Errorf("beneficiary mismatch: %v != %v", self.Beneficiary.Hex(), beneficiary.Hex()) 595 } 596 if self.Contract != contract { 597 return nil, fmt.Errorf("contract mismatch: %v != %v", self.Contract.Hex(), contract.Hex()) 598 } 599 600 amount := new(big.Int).Set(self.Amount) 601 if sum != nil { 602 amount.Sub(amount, sum) 603 if amount.Sign() <= 0 { 604 return nil, fmt.Errorf("incorrect amount: %v <= 0", amount) 605 } 606 } 607 608 pubKey, err := crypto.SigToPub(sigHash(self.Contract, beneficiary, self.Amount), self.Sig) 609 if err != nil { 610 return nil, fmt.Errorf("invalid signature: %v", err) 611 } 612 if !bytes.Equal(crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) { 613 return nil, fmt.Errorf("signer mismatch: %x != %x", crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) 614 } 615 return amount, nil 616 } 617 618 // v/r/s representation of signature 619 func sig2vrs(sig []byte) (v byte, r, s [32]byte) { 620 v = sig[64] + 27 621 copy(r[:], sig[:32]) 622 copy(s[:], sig[32:64]) 623 return 624 } 625 626 // Cash cashes the cheque by sending an Ethereum transaction. 627 func (self *Cheque) Cash(session *contract.ChequebookSession) (string, error) { 628 v, r, s := sig2vrs(self.Sig) 629 tx, err := session.Cash(self.Beneficiary, self.Amount, v, r, s) 630 if err != nil { 631 return "", err 632 } 633 return tx.Hash().Hex(), nil 634 } 635 636 // ValidateCode checks that the on-chain code at address matches the expected chequebook 637 // contract code. This is used to detect suicided chequebooks. 638 func ValidateCode(ctx context.Context, b Backend, address common.Address) (ok bool, err error) { 639 code, err := b.CodeAt(ctx, address, nil) 640 if err != nil { 641 return false, err 642 } 643 return bytes.Equal(code, common.FromHex(contract.ContractDeployedCode)), nil 644 }