github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/contracts/chequebook/cheque.go (about) 1 // Copyright 2016 The Spectrum Authors 2 // This file is part of the Spectrum library. 3 // 4 // The Spectrum 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 Spectrum 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 Spectrum 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/SmartMeshFoundation/Spectrum/accounts/abi/bind" 40 "github.com/SmartMeshFoundation/Spectrum/common" 41 "github.com/SmartMeshFoundation/Spectrum/common/hexutil" 42 "github.com/SmartMeshFoundation/Spectrum/contracts/chequebook/contract" 43 "github.com/SmartMeshFoundation/Spectrum/core/types" 44 "github.com/SmartMeshFoundation/Spectrum/crypto" 45 "github.com/SmartMeshFoundation/Spectrum/log" 46 "github.com/SmartMeshFoundation/Spectrum/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 380 go func() { 381 for { 382 select { 383 case <-quit: 384 return 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 } 399 400 // Outbox can issue cheques from a single contract to a single beneficiary. 401 type Outbox struct { 402 chequeBook *Chequebook 403 beneficiary common.Address 404 } 405 406 // NewOutbox creates an outbox. 407 func NewOutbox(chbook *Chequebook, beneficiary common.Address) *Outbox { 408 return &Outbox{chbook, beneficiary} 409 } 410 411 // Issue creates cheque. 412 func (self *Outbox) Issue(amount *big.Int) (swap.Promise, error) { 413 return self.chequeBook.Issue(self.beneficiary, amount) 414 } 415 416 // AutoDeposit enables auto-deposits on the underlying chequebook. 417 func (self *Outbox) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { 418 self.chequeBook.AutoDeposit(interval, threshold, buffer) 419 } 420 421 // Stop helps satisfy the swap.OutPayment interface. 422 func (self *Outbox) Stop() {} 423 424 // String implements fmt.Stringer. 425 func (self *Outbox) String() string { 426 return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().Hex(), self.beneficiary.Hex(), self.chequeBook.Balance()) 427 } 428 429 // Inbox can deposit, verify and cash cheques from a single contract to a single 430 // beneficiary. It is the incoming payment handler for peer to peer micropayments. 431 type Inbox struct { 432 lock sync.Mutex 433 contract common.Address // peer's chequebook contract 434 beneficiary common.Address // local peer's receiving address 435 sender common.Address // local peer's address to send cashing tx from 436 signer *ecdsa.PublicKey // peer's public key 437 txhash string // tx hash of last cashing tx 438 session *contract.ChequebookSession // abi contract backend with tx opts 439 quit chan bool // when closed causes autocash to stop 440 maxUncashed *big.Int // threshold that triggers autocashing 441 cashed *big.Int // cumulative amount cashed 442 cheque *Cheque // last cheque, nil if none yet received 443 log log.Logger // contextual logger with the contract address embedded 444 } 445 446 // NewInbox creates an Inbox. An Inboxes is not persisted, the cumulative sum is updated 447 // from blockchain when first cheque is received. 448 func NewInbox(prvKey *ecdsa.PrivateKey, contractAddr, beneficiary common.Address, signer *ecdsa.PublicKey, abigen bind.ContractBackend) (self *Inbox, err error) { 449 if signer == nil { 450 return nil, fmt.Errorf("signer is null") 451 } 452 chbook, err := contract.NewChequebook(contractAddr, abigen) 453 if err != nil { 454 return nil, err 455 } 456 transactOpts := bind.NewKeyedTransactor(prvKey) 457 transactOpts.GasLimit = gasToCash 458 session := &contract.ChequebookSession{ 459 Contract: chbook, 460 TransactOpts: *transactOpts, 461 } 462 sender := transactOpts.From 463 464 self = &Inbox{ 465 contract: contractAddr, 466 beneficiary: beneficiary, 467 sender: sender, 468 signer: signer, 469 session: session, 470 cashed: new(big.Int).Set(common.Big0), 471 log: log.New("contract", contractAddr), 472 } 473 self.log.Trace("New chequebook inbox initialized", "beneficiary", self.beneficiary, "signer", hexutil.Bytes(crypto.FromECDSAPub(signer))) 474 return 475 } 476 477 func (self *Inbox) String() string { 478 return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.contract.Hex(), self.beneficiary.Hex(), self.cheque.Amount) 479 } 480 481 // Stop quits the autocash goroutine. 482 func (self *Inbox) Stop() { 483 defer self.lock.Unlock() 484 self.lock.Lock() 485 if self.quit != nil { 486 close(self.quit) 487 self.quit = nil 488 } 489 } 490 491 // Cash attempts to cash the current cheque. 492 func (self *Inbox) Cash() (txhash string, err error) { 493 if self.cheque != nil { 494 txhash, err = self.cheque.Cash(self.session) 495 self.log.Trace("Cashing in chequebook cheque", "amount", self.cheque.Amount, "beneficiary", self.beneficiary) 496 self.cashed = self.cheque.Amount 497 } 498 return 499 } 500 501 // AutoCash (re)sets maximum time and amount which triggers cashing of the last uncashed 502 // cheque if maxUncashed is set to 0, then autocash on receipt. 503 func (self *Inbox) AutoCash(cashInterval time.Duration, maxUncashed *big.Int) { 504 defer self.lock.Unlock() 505 self.lock.Lock() 506 self.maxUncashed = maxUncashed 507 self.autoCash(cashInterval) 508 } 509 510 // autoCash starts a loop that periodically clears the last cheque 511 // if the peer is trusted. Clearing period could be 24h or a week. 512 // The caller must hold self.lock. 513 func (self *Inbox) autoCash(cashInterval time.Duration) { 514 if self.quit != nil { 515 close(self.quit) 516 self.quit = nil 517 } 518 // if maxUncashed is set to 0, then autocash on receipt 519 if cashInterval == time.Duration(0) || self.maxUncashed != nil && self.maxUncashed.Sign() == 0 { 520 return 521 } 522 523 ticker := time.NewTicker(cashInterval) 524 self.quit = make(chan bool) 525 quit := self.quit 526 527 go func() { 528 for { 529 select { 530 case <-quit: 531 return 532 case <-ticker.C: 533 self.lock.Lock() 534 if self.cheque != nil && self.cheque.Amount.Cmp(self.cashed) != 0 { 535 txhash, err := self.Cash() 536 if err == nil { 537 self.txhash = txhash 538 } 539 } 540 self.lock.Unlock() 541 } 542 } 543 }() 544 } 545 546 // Receive is called to deposit the latest cheque to the incoming Inbox. 547 // The given promise must be a *Cheque. 548 func (self *Inbox) Receive(promise swap.Promise) (*big.Int, error) { 549 ch := promise.(*Cheque) 550 551 defer self.lock.Unlock() 552 self.lock.Lock() 553 554 var sum *big.Int 555 if self.cheque == nil { 556 // the sum is checked against the blockchain once a cheque is received 557 tally, err := self.session.Sent(self.beneficiary) 558 if err != nil { 559 return nil, fmt.Errorf("inbox: error calling backend to set amount: %v", err) 560 } 561 sum = tally 562 } else { 563 sum = self.cheque.Amount 564 } 565 566 amount, err := ch.Verify(self.signer, self.contract, self.beneficiary, sum) 567 var uncashed *big.Int 568 if err == nil { 569 self.cheque = ch 570 571 if self.maxUncashed != nil { 572 uncashed = new(big.Int).Sub(ch.Amount, self.cashed) 573 if self.maxUncashed.Cmp(uncashed) < 0 { 574 self.Cash() 575 } 576 } 577 self.log.Trace("Received cheque in chequebook inbox", "amount", amount, "uncashed", uncashed) 578 } 579 580 return amount, err 581 } 582 583 // Verify verifies cheque for signer, contract, beneficiary, amount, valid signature. 584 func (self *Cheque) Verify(signerKey *ecdsa.PublicKey, contract, beneficiary common.Address, sum *big.Int) (*big.Int, error) { 585 log.Trace("Verifying chequebook cheque", "cheque", self, "sum", sum) 586 if sum == nil { 587 return nil, fmt.Errorf("invalid amount") 588 } 589 590 if self.Beneficiary != beneficiary { 591 return nil, fmt.Errorf("beneficiary mismatch: %v != %v", self.Beneficiary.Hex(), beneficiary.Hex()) 592 } 593 if self.Contract != contract { 594 return nil, fmt.Errorf("contract mismatch: %v != %v", self.Contract.Hex(), contract.Hex()) 595 } 596 597 amount := new(big.Int).Set(self.Amount) 598 if sum != nil { 599 amount.Sub(amount, sum) 600 if amount.Sign() <= 0 { 601 return nil, fmt.Errorf("incorrect amount: %v <= 0", amount) 602 } 603 } 604 605 pubKey, err := crypto.SigToPub(sigHash(self.Contract, beneficiary, self.Amount), self.Sig) 606 if err != nil { 607 return nil, fmt.Errorf("invalid signature: %v", err) 608 } 609 if !bytes.Equal(crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) { 610 return nil, fmt.Errorf("signer mismatch: %x != %x", crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) 611 } 612 return amount, nil 613 } 614 615 // v/r/s representation of signature 616 func sig2vrs(sig []byte) (v byte, r, s [32]byte) { 617 v = sig[64] + 27 618 copy(r[:], sig[:32]) 619 copy(s[:], sig[32:64]) 620 return 621 } 622 623 // Cash cashes the cheque by sending an Ethereum transaction. 624 func (self *Cheque) Cash(session *contract.ChequebookSession) (string, error) { 625 v, r, s := sig2vrs(self.Sig) 626 tx, err := session.Cash(self.Beneficiary, self.Amount, v, r, s) 627 if err != nil { 628 return "", err 629 } 630 return tx.Hash().Hex(), nil 631 } 632 633 // ValidateCode checks that the on-chain code at address matches the expected chequebook 634 // contract code. This is used to detect suicided chequebooks. 635 func ValidateCode(ctx context.Context, b Backend, address common.Address) (ok bool, err error) { 636 code, err := b.CodeAt(ctx, address, nil) 637 if err != nil { 638 return false, err 639 } 640 return bytes.Equal(code, common.FromHex(contract.ContractDeployedCode)), nil 641 }