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