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