github.com/intfoundation/intchain@v0.0.0-20220727031208-4316ad31ca73/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' INT Chain 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 --exc contract/mortal.sol:mortal,contract/owned.sol:owned --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/intfoundation/intchain/accounts/abi/bind" 40 "github.com/intfoundation/intchain/common" 41 "github.com/intfoundation/intchain/common/hexutil" 42 "github.com/intfoundation/intchain/contracts/chequebook/contract" 43 "github.com/intfoundation/intchain/core/types" 44 "github.com/intfoundation/intchain/crypto" 45 "github.com/intfoundation/intchain/log" 46 ) 47 48 // TODO(zelig): watch peer solvency and notify of bouncing cheques 49 // TODO(zelig): enable paying with cheque by signing off 50 51 // Some functionality requires interacting with the blockchain: 52 // * setting current balance on peer's chequebook 53 // * sending the transaction to cash the cheque 54 // * depositing ether to the chequebook 55 // * watching incoming ether 56 57 var ( 58 gasToCash = uint64(2000000) // gas cost of a cash transaction using chequebook 59 // gasToDeploy = uint64(3000000) 60 ) 61 62 // Backend wraps all methods required for chequebook operation. 63 type Backend interface { 64 bind.ContractBackend 65 TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) 66 BalanceAt(ctx context.Context, address common.Address, blockNum *big.Int) (*big.Int, error) 67 } 68 69 // Cheque represents a payment promise to a single beneficiary. 70 type Cheque struct { 71 Contract common.Address // address of chequebook, needed to avoid cross-contract submission 72 Beneficiary common.Address 73 Amount *big.Int // cumulative amount of all funds sent 74 Sig []byte // signature Sign(Keccak256(contract, beneficiary, amount), prvKey) 75 } 76 77 func (self *Cheque) String() string { 78 //return fmt.Sprintf("contract: %s, beneficiary: %s, amount: %v, signature: %x", self.Contract.Hex(), self.Beneficiary.Hex(), self.Amount, self.Sig) 79 return fmt.Sprintf("contract: %s, beneficiary: %s, amount: %v, signature: %x", self.Contract.String(), self.Beneficiary.String(), 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 return fmt.Sprintf("contract: %s, owner: %s, balance: %v, signer: %x", self.contractAddr.String(), self.owner.String(), self.balance, self.prvKey.PublicKey) 115 } 116 117 // NewChequebook creates a new Chequebook. 118 func NewChequebook(path string, contractAddr common.Address, prvKey *ecdsa.PrivateKey, backend Backend) (self *Chequebook, err error) { 119 balance := new(big.Int) 120 sent := make(map[common.Address]*big.Int) 121 122 chbook, err := contract.NewChequebook(contractAddr, backend) 123 if err != nil { 124 return nil, err 125 } 126 transactOpts := bind.NewKeyedTransactor(prvKey) 127 session := &contract.ChequebookSession{ 128 Contract: chbook, 129 TransactOpts: *transactOpts, 130 } 131 132 self = &Chequebook{ 133 prvKey: prvKey, 134 balance: balance, 135 contractAddr: contractAddr, 136 sent: sent, 137 path: path, 138 backend: backend, 139 owner: transactOpts.From, 140 contract: chbook, 141 session: session, 142 log: log.New("contract", contractAddr), 143 } 144 145 if (contractAddr != common.Address{}) { 146 self.setBalanceFromBlockChain() 147 self.log.Trace("New chequebook initialised", "owner", self.owner, "balance", self.balance) 148 } 149 return 150 } 151 152 func (self *Chequebook) setBalanceFromBlockChain() { 153 balance, err := self.backend.BalanceAt(context.TODO(), self.contractAddr, nil) 154 if err != nil { 155 log.Error("Failed to retrieve chequebook balance", "err", err) 156 } else { 157 self.balance.Set(balance) 158 } 159 } 160 161 // LoadChequebook loads a chequebook from disk (file path). 162 func LoadChequebook(path string, prvKey *ecdsa.PrivateKey, backend Backend, checkBalance bool) (self *Chequebook, err error) { 163 var data []byte 164 data, err = ioutil.ReadFile(path) 165 if err != nil { 166 return 167 } 168 self, _ = NewChequebook(path, common.Address{}, prvKey, backend) 169 170 err = json.Unmarshal(data, self) 171 if err != nil { 172 return nil, err 173 } 174 if checkBalance { 175 self.setBalanceFromBlockChain() 176 } 177 log.Trace("Loaded chequebook from disk", "path", path) 178 179 return 180 } 181 182 // chequebookFile is the JSON representation of a chequebook. 183 type chequebookFile struct { 184 Balance string 185 Contract string 186 Owner string 187 Sent map[string]string 188 } 189 190 // UnmarshalJSON deserialises a chequebook. 191 func (self *Chequebook) UnmarshalJSON(data []byte) error { 192 var file chequebookFile 193 err := json.Unmarshal(data, &file) 194 if err != nil { 195 return err 196 } 197 _, ok := self.balance.SetString(file.Balance, 10) 198 if !ok { 199 return fmt.Errorf("cumulative amount sent: unable to convert string to big integer: %v", file.Balance) 200 } 201 self.contractAddr = common.HexToAddress(file.Contract) 202 for addr, sent := range file.Sent { 203 self.sent[common.HexToAddress(addr)], ok = new(big.Int).SetString(sent, 10) 204 if !ok { 205 return fmt.Errorf("beneficiary %v cumulative amount sent: unable to convert string to big integer: %v", addr, sent) 206 } 207 } 208 return nil 209 } 210 211 // MarshalJSON serialises a chequebook. 212 func (self *Chequebook) MarshalJSON() ([]byte, error) { 213 var file = &chequebookFile{ 214 Balance: self.balance.String(), 215 //Contract: self.contractAddr.Hex(), 216 Contract: self.contractAddr.String(), 217 //Owner: self.owner.Hex(), 218 Owner: self.owner.String(), 219 Sent: make(map[string]string), 220 } 221 for addr, sent := range self.sent { 222 //file.Sent[addr.Hex()] = sent.String() 223 file.Sent[addr.String()] = sent.String() 224 } 225 return json.Marshal(file) 226 } 227 228 // Save persists the chequebook on disk, remembering balance, contract address and 229 // cumulative amount of funds sent for each beneficiary. 230 func (self *Chequebook) Save() (err error) { 231 data, err := json.MarshalIndent(self, "", " ") 232 if err != nil { 233 return err 234 } 235 self.log.Trace("Saving chequebook to disk", self.path) 236 237 return ioutil.WriteFile(self.path, data, os.ModePerm) 238 } 239 240 // Stop quits the autodeposit go routine to terminate 241 func (self *Chequebook) Stop() { 242 defer self.lock.Unlock() 243 self.lock.Lock() 244 if self.quit != nil { 245 close(self.quit) 246 self.quit = nil 247 } 248 } 249 250 // Issue creates a cheque signed by the chequebook owner's private key. The 251 // signer commits to a contract (one that they own), a beneficiary and amount. 252 func (self *Chequebook) Issue(beneficiary common.Address, amount *big.Int) (ch *Cheque, err error) { 253 defer self.lock.Unlock() 254 self.lock.Lock() 255 256 if amount.Sign() <= 0 { 257 return nil, fmt.Errorf("amount must be greater than zero (%v)", amount) 258 } 259 if self.balance.Cmp(amount) < 0 { 260 err = fmt.Errorf("insufficient funds to issue cheque for amount: %v. balance: %v", amount, self.balance) 261 } else { 262 var sig []byte 263 sent, found := self.sent[beneficiary] 264 if !found { 265 sent = new(big.Int) 266 self.sent[beneficiary] = sent 267 } 268 sum := new(big.Int).Set(sent) 269 sum.Add(sum, amount) 270 271 sig, err = crypto.Sign(sigHash(self.contractAddr, beneficiary, sum), self.prvKey) 272 if err == nil { 273 ch = &Cheque{ 274 Contract: self.contractAddr, 275 Beneficiary: beneficiary, 276 Amount: sum, 277 Sig: sig, 278 } 279 sent.Set(sum) 280 self.balance.Sub(self.balance, amount) // subtract amount from balance 281 } 282 } 283 284 // auto deposit if threshold is set and balance is less then threshold 285 // note this is called even if issuing cheque fails 286 // so we reattempt depositing 287 if self.threshold != nil { 288 if self.balance.Cmp(self.threshold) < 0 { 289 send := new(big.Int).Sub(self.buffer, self.balance) 290 self.deposit(send) 291 } 292 } 293 294 return 295 } 296 297 // Cash is a convenience method to cash any cheque. 298 func (self *Chequebook) Cash(ch *Cheque) (txhash string, err error) { 299 return ch.Cash(self.session) 300 } 301 302 // data to sign: contract address, beneficiary, cumulative amount of funds ever sent 303 func sigHash(contract, beneficiary common.Address, sum *big.Int) []byte { 304 bigamount := sum.Bytes() 305 if len(bigamount) > 32 { 306 return nil 307 } 308 var amount32 [32]byte 309 copy(amount32[32-len(bigamount):32], bigamount) 310 input := append(contract.Bytes(), beneficiary.Bytes()...) 311 input = append(input, amount32[:]...) 312 return crypto.Keccak256(input) 313 } 314 315 // Balance returns the current balance of the chequebook. 316 func (self *Chequebook) Balance() *big.Int { 317 defer self.lock.Unlock() 318 self.lock.Lock() 319 return new(big.Int).Set(self.balance) 320 } 321 322 // Owner returns the owner account of the chequebook. 323 func (self *Chequebook) Owner() common.Address { 324 return self.owner 325 } 326 327 // Address returns the on-chain contract address of the chequebook. 328 func (self *Chequebook) Address() common.Address { 329 return self.contractAddr 330 } 331 332 // Deposit deposits money to the chequebook account. 333 func (self *Chequebook) Deposit(amount *big.Int) (string, error) { 334 defer self.lock.Unlock() 335 self.lock.Lock() 336 return self.deposit(amount) 337 } 338 339 // deposit deposits amount to the chequebook account. 340 // The caller must hold self.lock. 341 func (self *Chequebook) deposit(amount *big.Int) (string, error) { 342 // since the amount is variable here, we do not use sessions 343 depositTransactor := bind.NewKeyedTransactor(self.prvKey) 344 depositTransactor.Value = amount 345 chbookRaw := &contract.ChequebookRaw{Contract: self.contract} 346 tx, err := chbookRaw.Transfer(depositTransactor) 347 if err != nil { 348 self.log.Warn("Failed to fund chequebook", "amount", amount, "balance", self.balance, "target", self.buffer, "err", err) 349 return "", err 350 } 351 // assume that transaction is actually successful, we add the amount to balance right away 352 self.balance.Add(self.balance, amount) 353 self.log.Trace("Deposited funds to chequebook", "amount", amount, "balance", self.balance, "target", self.buffer) 354 return tx.Hash().Hex(), nil 355 } 356 357 // AutoDeposit (re)sets interval time and amount which triggers sending funds to the 358 // chequebook. Contract backend needs to be set if threshold is not less than buffer, then 359 // deposit will be triggered on every new cheque issued. 360 func (self *Chequebook) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { 361 defer self.lock.Unlock() 362 self.lock.Lock() 363 self.threshold = threshold 364 self.buffer = buffer 365 self.autoDeposit(interval) 366 } 367 368 // autoDeposit starts a goroutine that periodically sends funds to the chequebook 369 // contract caller holds the lock the go routine terminates if Chequebook.quit is closed. 370 func (self *Chequebook) autoDeposit(interval time.Duration) { 371 if self.quit != nil { 372 close(self.quit) 373 self.quit = nil 374 } 375 // if threshold >= balance autodeposit after every cheque issued 376 if interval == time.Duration(0) || self.threshold != nil && self.buffer != nil && self.threshold.Cmp(self.buffer) >= 0 { 377 return 378 } 379 380 ticker := time.NewTicker(interval) 381 self.quit = make(chan bool) 382 quit := self.quit 383 384 go func() { 385 for { 386 select { 387 case <-quit: 388 return 389 case <-ticker.C: 390 self.lock.Lock() 391 if self.balance.Cmp(self.buffer) < 0 { 392 amount := new(big.Int).Sub(self.buffer, self.balance) 393 txhash, err := self.deposit(amount) 394 if err == nil { 395 self.txhash = txhash 396 } 397 } 398 self.lock.Unlock() 399 } 400 } 401 }() 402 } 403 404 // Outbox can issue cheques from a single contract to a single beneficiary. 405 type Outbox struct { 406 chequeBook *Chequebook 407 beneficiary common.Address 408 } 409 410 // NewOutbox creates an outbox. 411 func NewOutbox(chbook *Chequebook, beneficiary common.Address) *Outbox { 412 return &Outbox{chbook, beneficiary} 413 } 414 415 // AutoDeposit enables auto-deposits on the underlying chequebook. 416 func (self *Outbox) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { 417 self.chequeBook.AutoDeposit(interval, threshold, buffer) 418 } 419 420 // Stop helps satisfy the swap.OutPayment interface. 421 func (self *Outbox) Stop() {} 422 423 // String implements fmt.Stringer. 424 func (self *Outbox) String() string { 425 //return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().Hex(), self.beneficiary.Hex(), self.chequeBook.Balance()) 426 return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().String(), self.beneficiary.String(), 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 return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.contract.String(), self.beneficiary.String(), self.cheque.Amount) 480 } 481 482 // Stop quits the autocash goroutine. 483 func (self *Inbox) Stop() { 484 defer self.lock.Unlock() 485 self.lock.Lock() 486 if self.quit != nil { 487 close(self.quit) 488 self.quit = nil 489 } 490 } 491 492 // Cash attempts to cash the current cheque. 493 func (self *Inbox) Cash() (txhash string, err error) { 494 if self.cheque != nil { 495 txhash, err = self.cheque.Cash(self.session) 496 self.log.Trace("Cashing in chequebook cheque", "amount", self.cheque.Amount, "beneficiary", self.beneficiary) 497 self.cashed = self.cheque.Amount 498 } 499 return 500 } 501 502 // AutoCash (re)sets maximum time and amount which triggers cashing of the last uncashed 503 // cheque if maxUncashed is set to 0, then autocash on receipt. 504 func (self *Inbox) AutoCash(cashInterval time.Duration, maxUncashed *big.Int) { 505 defer self.lock.Unlock() 506 self.lock.Lock() 507 self.maxUncashed = maxUncashed 508 self.autoCash(cashInterval) 509 } 510 511 // autoCash starts a loop that periodically clears the last cheque 512 // if the peer is trusted. Clearing period could be 24h or a week. 513 // The caller must hold self.lock. 514 func (self *Inbox) autoCash(cashInterval time.Duration) { 515 if self.quit != nil { 516 close(self.quit) 517 self.quit = nil 518 } 519 // if maxUncashed is set to 0, then autocash on receipt 520 if cashInterval == time.Duration(0) || self.maxUncashed != nil && self.maxUncashed.Sign() == 0 { 521 return 522 } 523 524 ticker := time.NewTicker(cashInterval) 525 self.quit = make(chan bool) 526 quit := self.quit 527 528 go func() { 529 for { 530 select { 531 case <-quit: 532 return 533 case <-ticker.C: 534 self.lock.Lock() 535 if self.cheque != nil && self.cheque.Amount.Cmp(self.cashed) != 0 { 536 txhash, err := self.Cash() 537 if err == nil { 538 self.txhash = txhash 539 } 540 } 541 self.lock.Unlock() 542 } 543 } 544 }() 545 } 546 547 // Verify verifies cheque for signer, contract, beneficiary, amount, valid signature. 548 func (self *Cheque) Verify(signerKey *ecdsa.PublicKey, contract, beneficiary common.Address, sum *big.Int) (*big.Int, error) { 549 log.Trace("Verifying chequebook cheque", "cheque", self, "sum", sum) 550 if sum == nil { 551 return nil, fmt.Errorf("invalid amount") 552 } 553 554 if self.Beneficiary != beneficiary { 555 //return nil, fmt.Errorf("beneficiary mismatch: %v != %v", self.Beneficiary.Hex(), beneficiary.Hex()) 556 return nil, fmt.Errorf("beneficiary mismatch: %v != %v", self.Beneficiary.String(), beneficiary.String()) 557 } 558 if self.Contract != contract { 559 //return nil, fmt.Errorf("contract mismatch: %v != %v", self.Contract.Hex(), contract.Hex()) 560 return nil, fmt.Errorf("contract mismatch: %v != %v", self.Contract.String(), contract.String()) 561 } 562 563 amount := new(big.Int).Set(self.Amount) 564 if sum != nil { 565 amount.Sub(amount, sum) 566 if amount.Sign() <= 0 { 567 return nil, fmt.Errorf("incorrect amount: %v <= 0", amount) 568 } 569 } 570 571 pubKey, err := crypto.SigToPub(sigHash(self.Contract, beneficiary, self.Amount), self.Sig) 572 if err != nil { 573 return nil, fmt.Errorf("invalid signature: %v", err) 574 } 575 if !bytes.Equal(crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) { 576 return nil, fmt.Errorf("signer mismatch: %x != %x", crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) 577 } 578 return amount, nil 579 } 580 581 // v/r/s representation of signature 582 func sig2vrs(sig []byte) (v byte, r, s [32]byte) { 583 v = sig[64] + 27 584 copy(r[:], sig[:32]) 585 copy(s[:], sig[32:64]) 586 return 587 } 588 589 // Cash cashes the cheque by sending an Ethereum transaction. 590 func (self *Cheque) Cash(session *contract.ChequebookSession) (string, error) { 591 v, r, s := sig2vrs(self.Sig) 592 tx, err := session.Cash(self.Beneficiary, self.Amount, v, r, s) 593 if err != nil { 594 return "", err 595 } 596 return tx.Hash().Hex(), nil 597 } 598 599 // ValidateCode checks that the on-chain code at address matches the expected chequebook 600 // contract code. This is used to detect suicided chequebooks. 601 func ValidateCode(ctx context.Context, b Backend, address common.Address) (ok bool, err error) { 602 code, err := b.CodeAt(ctx, address, nil) 603 if err != nil { 604 return false, err 605 } 606 return bytes.Equal(code, common.FromHex(contract.ContractDeployedCode)), nil 607 }