mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
contracts/accountbook: implement accountbook contract
This commit is contained in:
parent
08611cfd75
commit
a2cb8e8ba2
33 changed files with 3971 additions and 49 deletions
|
|
@ -19,8 +19,10 @@ package bind
|
|||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/external"
|
||||
|
|
@ -94,3 +96,19 @@ func NewClefTransactor(clef *external.ExternalSigner, account accounts.Account)
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRawTransactor is a utility method to easily create a transaction signer
|
||||
// with a raw signer function.
|
||||
func NewRawTransactor(signFn func(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error), account accounts.Account) *TransactOpts {
|
||||
return &TransactOpts{
|
||||
From: account.Address,
|
||||
Signer: func(signer types.Signer, address common.Address, transaction *types.Transaction) (*types.Transaction, error) {
|
||||
if address != account.Address {
|
||||
fmt.Println("want", account.Address.Hex(), "got", address.Hex())
|
||||
|
||||
return nil, errors.New("not authorized to sign this account")
|
||||
}
|
||||
return signFn(account, transaction, nil) // Clef enforces its own chain id
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
accounts/external/backend.go
vendored
12
accounts/external/backend.go
vendored
|
|
@ -17,6 +17,7 @@
|
|||
package external
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
|
@ -130,6 +131,11 @@ func (api *ExternalSigner) Accounts() []accounts.Account {
|
|||
|
||||
func (api *ExternalSigner) Contains(account accounts.Account) bool {
|
||||
api.cacheMu.RLock()
|
||||
if api.cache == nil {
|
||||
api.cacheMu.RUnlock()
|
||||
api.Accounts()
|
||||
api.cacheMu.RLock()
|
||||
}
|
||||
defer api.cacheMu.RUnlock()
|
||||
for _, a := range api.cache {
|
||||
if a.Address == account.Address && (account.URL == (accounts.URL{}) || account.URL == api.URL()) {
|
||||
|
|
@ -155,10 +161,14 @@ func (api *ExternalSigner) signHash(account accounts.Account, hash []byte) ([]by
|
|||
func (api *ExternalSigner) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
|
||||
var res hexutil.Bytes
|
||||
var signAddress = common.NewMixedcaseAddress(account.Address)
|
||||
var param = make(map[string]interface{})
|
||||
if err := json.Unmarshal(data, ¶m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := api.client.Call(&res, "account_signData",
|
||||
mimeType,
|
||||
&signAddress, // Need to use the pointer here, because of how MarshalJSON is defined
|
||||
hexutil.Encode(data)); err != nil {
|
||||
param); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If V is on 27/28-form, convert to to 0/1 for Clique
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ var (
|
|||
utils.UltraLightServersFlag,
|
||||
utils.UltraLightFractionFlag,
|
||||
utils.UltraLightOnlyAnnounceFlag,
|
||||
utils.ServiceChargeFlag,
|
||||
utils.ServicePaymentFlag,
|
||||
utils.LightAddressFlag,
|
||||
utils.WhitelistFlag,
|
||||
utils.CacheFlag,
|
||||
utils.CacheDatabaseFlag,
|
||||
|
|
@ -337,7 +340,7 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
|||
if err := stack.Service(ðService); err != nil {
|
||||
utils.Fatalf("Failed to retrieve ethereum service: %v", err)
|
||||
}
|
||||
ethService.SetContractBackend(ethClient)
|
||||
ethService.SetBackends(ethClient, ethClient)
|
||||
}
|
||||
// Set contract backend for les service if local node is
|
||||
// running as a light client.
|
||||
|
|
@ -346,7 +349,7 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
|||
if err := stack.Service(&lesService); err != nil {
|
||||
utils.Fatalf("Failed to retrieve light ethereum service: %v", err)
|
||||
}
|
||||
lesService.SetContractBackend(ethClient)
|
||||
lesService.SetBackends(ethClient, ethClient)
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ var AppHelpFlagGroups = []flagGroup{
|
|||
utils.UltraLightServersFlag,
|
||||
utils.UltraLightFractionFlag,
|
||||
utils.UltraLightOnlyAnnounceFlag,
|
||||
utils.ServiceChargeFlag,
|
||||
utils.ServicePaymentFlag,
|
||||
utils.LightAddressFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -282,6 +282,19 @@ var (
|
|||
Name: "ulc.onlyannounce",
|
||||
Usage: "Ultra light server sends announcements only",
|
||||
}
|
||||
// Les server incentivization settings
|
||||
ServiceChargeFlag = cli.BoolFlag{
|
||||
Name: "light.charge",
|
||||
Usage: "Indicator whether to charge for light client service",
|
||||
}
|
||||
ServicePaymentFlag = cli.BoolFlag{
|
||||
Name: "light.pay",
|
||||
Usage: "Indicator whether to pay for light server service",
|
||||
}
|
||||
LightAddressFlag = cli.StringFlag{
|
||||
Name: "light.address",
|
||||
Usage: "Account address of the light server or client which used to pay the fee or charge",
|
||||
}
|
||||
// Ethash settings
|
||||
EthashCacheDirFlag = DirectoryFlag{
|
||||
Name: "ethash.cachedir",
|
||||
|
|
@ -1001,6 +1014,15 @@ func setLes(ctx *cli.Context, cfg *eth.Config) {
|
|||
if ctx.GlobalIsSet(UltraLightOnlyAnnounceFlag.Name) {
|
||||
cfg.UltraLightOnlyAnnounce = ctx.GlobalBool(UltraLightOnlyAnnounceFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(ServiceChargeFlag.Name) {
|
||||
cfg.LightServiceCharge = ctx.GlobalBool(ServiceChargeFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(ServicePaymentFlag.Name) {
|
||||
cfg.LightServicePay = ctx.GlobalBool(ServicePaymentFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(LightAddressFlag.Name) {
|
||||
cfg.LightAddress = common.HexToAddress(ctx.GlobalString(LightAddressFlag.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// makeDatabaseHandles raises out the number of allowed file handles per process
|
||||
|
|
@ -1531,7 +1553,7 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
|||
err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
fullNode, err := eth.New(ctx, cfg)
|
||||
if fullNode != nil && cfg.LightServ > 0 {
|
||||
ls, _ := les.NewLesServer(fullNode, cfg)
|
||||
ls, _ := les.NewLesServer(ctx, fullNode, cfg)
|
||||
fullNode.AddLesServer(ls)
|
||||
}
|
||||
return fullNode, err
|
||||
|
|
|
|||
224
contracts/accountbook/accountbook.go
Normal file
224
contracts/accountbook/accountbook.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// package accountbook implements the contract based micropayment for les server.
|
||||
package accountbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/contracts/accountbook/contract"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
const (
|
||||
ChallengeTimeWindow = 64 // The default challenge block numbers, which euqals to 16mins
|
||||
deployTimeout = 5 * time.Minute // The maxmium waiting time for contract deployment.
|
||||
)
|
||||
|
||||
// Cheque is a document that orders a bank(contract) to pay a specific amount
|
||||
// of money from a person's account to the person in whose name the cheque has
|
||||
// been issued(contract owner). The cheque is signed by drawer so that he can't
|
||||
// deny it.
|
||||
//
|
||||
// What is different from traditional cheques is: the amount of the cheque is
|
||||
// cumulative. So that contract can easily check whether the cheque is double-cash
|
||||
// by the payee.
|
||||
//
|
||||
// TODO(rjl493456442) add CHAINID
|
||||
type Cheque struct {
|
||||
Drawer common.Address // The drawer of the cheque
|
||||
ContractAddr common.Address // The address of the accountbook contract(bank address)
|
||||
Amount *big.Int // The cumulative amount of the issued cheque
|
||||
Sig [crypto.SignatureLength]byte
|
||||
}
|
||||
|
||||
type chequeRLP struct {
|
||||
ContractAddr common.Address // The address of the accountbook contract(bank address)
|
||||
Amount *big.Int // The cumulative amount of the issued cheque
|
||||
Sig [crypto.SignatureLength]byte
|
||||
}
|
||||
|
||||
// EncodeRLP implements rlp.Encoder, and flattens the necessary fields of a cheque
|
||||
// into an RLP stream.
|
||||
func (c *Cheque) EncodeRLP(w io.Writer) error {
|
||||
return rlp.Encode(w, &chequeRLP{ContractAddr: c.ContractAddr, Amount: c.Amount, Sig: c.Sig})
|
||||
}
|
||||
|
||||
// DecodeRLP implements rlp.Decoder, and loads the rlp-encoded fields of a cheque
|
||||
// from an RLP stream.
|
||||
func (c *Cheque) DecodeRLP(s *rlp.Stream) error {
|
||||
var dec chequeRLP
|
||||
if err := s.Decode(&dec); err != nil {
|
||||
return err
|
||||
}
|
||||
c.ContractAddr, c.Amount, c.Sig = dec.ContractAddr, dec.Amount, dec.Sig
|
||||
// If the cheque doesn't contain a signature, skip resolving the drawer address.
|
||||
if c.Sig == [65]byte{} {
|
||||
return nil
|
||||
}
|
||||
drawer, err := c.recoverDrawer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Drawer = drawer
|
||||
return nil
|
||||
}
|
||||
|
||||
// recoverDrawer resolves the drawer address from the cheque content
|
||||
// and signed signature.
|
||||
func (c *Cheque) recoverDrawer() (common.Address, error) {
|
||||
// EIP 191 style signatures
|
||||
//
|
||||
// Arguments when calculating hash to validate
|
||||
// 1: byte(0x19) - the initial 0x19 byte
|
||||
// 2: byte(0) - the version byte (data with intended validator)
|
||||
// 3: this - the validator address
|
||||
// -- Application specific data
|
||||
// 4: amount(uint256) big endian 32bytes
|
||||
buf := make([]byte, 32)
|
||||
copy(buf[32-len(c.Amount.Bytes()):], c.Amount.Bytes())
|
||||
data := append([]byte{0x19, 0x00}, append(c.ContractAddr.Bytes(), buf...)...)
|
||||
|
||||
// Transform V from 27/28 to 0/1 according to the yellow paper
|
||||
c.Sig[64] -= 27
|
||||
defer func() {
|
||||
c.Sig[64] += 27
|
||||
}()
|
||||
pubkey, err := crypto.SigToPub(crypto.Keccak256(data), c.Sig[:])
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
return crypto.PubkeyToAddress(*pubkey), nil
|
||||
}
|
||||
|
||||
// validate verifies whether the cheque is signed properly and all fields
|
||||
// are filled.
|
||||
func (c *Cheque) validate(chanAddr common.Address) error {
|
||||
drawer, err := c.recoverDrawer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if drawer != c.Drawer {
|
||||
return errors.New("invalid signature")
|
||||
}
|
||||
if c.ContractAddr != chanAddr {
|
||||
return errors.New("unsolicited cheque")
|
||||
}
|
||||
if c.Amount == nil {
|
||||
return errors.New("incomplete cheque")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sign generates the digital signature for cheque by clef. It's a bit
|
||||
// different with signWithKey, we need to construct a RPC call with clef
|
||||
// format.
|
||||
func (c *Cheque) sign(signFn func(data []byte) ([]byte, error)) error {
|
||||
// EIP 191 style signatures
|
||||
//
|
||||
// Arguments when calculating hash to validate
|
||||
// 1: byte(0x19) - the initial 0x19 byte
|
||||
// 2: byte(0) - the version byte (data with intended validator)
|
||||
// 3: this - the validator address
|
||||
// -- Application specific data
|
||||
// 4 : amount(uint256) big endian 32bytes
|
||||
p := make(map[string]string)
|
||||
p["address"] = c.ContractAddr.Hex()
|
||||
buf := make([]byte, 32)
|
||||
copy(buf[32-len(c.Amount.Bytes()):], c.Amount.Bytes())
|
||||
p["message"] = hexutil.Encode(buf)
|
||||
encoded, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sig, err := signFn(encoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy(c.Sig[:], sig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// signWithKey signes the cheque with privatekey. Only use it in testing.
|
||||
func (c *Cheque) signWithKey(signFn func(digestHash []byte) ([]byte, error)) error {
|
||||
// EIP 191 style signatures
|
||||
//
|
||||
// Arguments when calculating hash to validate
|
||||
// 1: byte(0x19) - the initial 0x19 byte
|
||||
// 2: byte(0) - the version byte (data with intended validator)
|
||||
// 3: this - the validator address
|
||||
// -- Application specific data
|
||||
// 4 : amount(uint256) big endian 32bytes
|
||||
buf := make([]byte, 32)
|
||||
copy(buf[32-len(c.Amount.Bytes()):], c.Amount.Bytes())
|
||||
data := append([]byte{0x19, 0x00}, append(c.ContractAddr.Bytes(), buf...)...)
|
||||
sig, err := signFn(crypto.Keccak256(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sig[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||
copy(c.Sig[:], sig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AccountBook represents a contract instance which holds all drawer's deposits.
|
||||
type AccountBook struct {
|
||||
address common.Address
|
||||
contract *contract.AccountBook
|
||||
}
|
||||
|
||||
// NewAccountBook deploys a new accountbook contract or initializes
|
||||
// a exist contract by given address.
|
||||
//
|
||||
// Note this function can take several minutes for execution.
|
||||
func newAccountBook(address common.Address, contractBackend bind.ContractBackend) (*AccountBook, error) {
|
||||
log.Info("Initialized accountbook contract", "address", address)
|
||||
c, err := contract.NewAccountBook(address, contractBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountBook{contract: c, address: address}, nil
|
||||
}
|
||||
|
||||
// deployAccountBook deploys the accountbook smart contract and waits the transaction
|
||||
// is confirmed by network.
|
||||
func deployAccountBook(auth *bind.TransactOpts, contractBackend bind.ContractBackend, deployBackend bind.DeployBackend) (common.Address, error) {
|
||||
log.Info("Deploying accountbook contract")
|
||||
start := time.Now()
|
||||
_, tx, _, err := contract.DeployAccountBook(auth, contractBackend, uint64(ChallengeTimeWindow))
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
context, cancelFn := context.WithTimeout(context.Background(), deployTimeout)
|
||||
defer cancelFn()
|
||||
addr, err := bind.WaitDeployed(context, deployBackend, tx)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
log.Info("Deployed accountbook contract", "address", addr, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||
return addr, nil
|
||||
}
|
||||
171
contracts/accountbook/chequedb.go
Normal file
171
contracts/accountbook/chequedb.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accountbook
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
var (
|
||||
// Database schema definitions
|
||||
// +-----------+
|
||||
// > Drawer1(client) ---------> | cheque1 |
|
||||
// Cheque Drawee(Les server) -/ +-----------+
|
||||
// -/
|
||||
// +------------+ +------------+ -/ +-----------+
|
||||
// |server addr1|----->| Contract1 |-/-------> Drawer2(client) ---------> | cheque2 |
|
||||
// +------------+ +------------+ -\ +-----------+
|
||||
// -\
|
||||
// -\ +-----------+
|
||||
// > Drawer3(client) ---------> | cheque3 |
|
||||
// +-----------+
|
||||
// ...
|
||||
// +------------+ +------------+
|
||||
// |server addrn|----->| Contractn |
|
||||
// +------------+ +------------+
|
||||
//
|
||||
// Cheque Drawer(Light client)
|
||||
// +---------------+
|
||||
// -> Drawee1(server) ---------> | last issued |
|
||||
// +------------+ -/ +---------------+
|
||||
// |client addr1|-/---> Drawee2(server) ---------> | last issued |
|
||||
// +------------+ -\ +---------------+
|
||||
// -> Drawee3(server) ---------> | last issued |
|
||||
// +---------------+
|
||||
// ...
|
||||
// +------------+
|
||||
// |client addrn|
|
||||
// +------------+
|
||||
contractAddrPrefix = []byte("-a") // contractAddrPrefix + deployer(20bytes) -> contract_addr
|
||||
chequePrefix = []byte("-c") // chequePrefix + contract_addr(20bytes) + drawer_id(20bytes) -> cheque
|
||||
issuedPrefix = []byte("-i") // issuedPrefix + drawer_id(20bytes) + contract_addr(20bytes) -> big-endian number
|
||||
)
|
||||
|
||||
// chequeDB keeps all signed cheques issued by customers. It's very important
|
||||
// to save the cheques properly, otherwise the owner of accountbook can't claim
|
||||
// the money back.
|
||||
//
|
||||
// Cheques are cumulatively confirmed, so only the latest version needs to be stored.
|
||||
type chequeDB struct {
|
||||
db ethdb.Database
|
||||
}
|
||||
|
||||
// newChequeDB intiailises the chequedb with given db handler.
|
||||
func newChequeDB(db ethdb.Database) *chequeDB { return &chequeDB{db: db} }
|
||||
|
||||
// readContractAddr returns the contract address deployed by specified deployer.
|
||||
func (db *chequeDB) readContractAddr(deployer common.Address) *common.Address {
|
||||
blob, err := db.db.Get(append(contractAddrPrefix, deployer.Bytes()...))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(blob) != common.AddressLength {
|
||||
return nil
|
||||
}
|
||||
addr := common.BytesToAddress(blob)
|
||||
return &addr
|
||||
}
|
||||
|
||||
// writeContractAddr writes the contract address which deployed by current address.
|
||||
func (db *chequeDB) writeContractAddr(deployer, contractAddr common.Address) {
|
||||
if err := db.db.Put(append(contractAddrPrefix, deployer.Bytes()...), contractAddr.Bytes()); err != nil {
|
||||
log.Crit("Failed to write contract address", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// readCheque returns the last issued cheque for the specified drawer.
|
||||
// If there is no local_addr => contract_addr mapping, it means we haven't
|
||||
// deployed the contract yet.
|
||||
func (db *chequeDB) readCheque(contractAddr, drawer common.Address) *Cheque {
|
||||
blob, err := db.db.Get(append(append(chequePrefix, contractAddr.Bytes()...), drawer.Bytes()...))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var cheque Cheque
|
||||
if err := rlp.DecodeBytes(blob, &cheque); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &cheque
|
||||
}
|
||||
|
||||
// writeCheque writes the last issued cheque from the specific drawer
|
||||
// into disk.
|
||||
func (db *chequeDB) writeCheque(contractAddr, drawer common.Address, cheque *Cheque) {
|
||||
blob, err := rlp.EncodeToBytes(cheque)
|
||||
if err != nil {
|
||||
log.Crit("Failed to encode cheque", "error", err)
|
||||
}
|
||||
err = db.db.Put(append(append(chequePrefix, contractAddr.Bytes()...), drawer.Bytes()...), blob)
|
||||
if err != nil {
|
||||
log.Crit("Failed to store cheque", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// readLastIssued returns the last issued amount by local address to
|
||||
// specified contract address.
|
||||
func (db *chequeDB) readLastIssued(drawer, contractAddr common.Address) *big.Int {
|
||||
blob, err := db.db.Get(append(append(issuedPrefix, drawer.Bytes()...), contractAddr.Bytes()...))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return new(big.Int).SetBytes(blob)
|
||||
}
|
||||
|
||||
// writeLastIssued writes the last issued amount by local address to
|
||||
// specified contract address into the disk.
|
||||
func (db *chequeDB) writeLastIssued(drawer, contractAddr common.Address, amount *big.Int) {
|
||||
if err := db.db.Put(append(append(issuedPrefix, drawer.Bytes()...), contractAddr.Bytes()...), amount.Bytes()); err != nil {
|
||||
log.Crit("Failed to store last issue amount", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// allCheques returns all received cheques from different drawers.
|
||||
func (db *chequeDB) allCheques(contractAddr common.Address) (cheques []*Cheque) {
|
||||
iter := db.db.NewIteratorWithPrefix(append(chequePrefix, contractAddr.Bytes()...))
|
||||
defer iter.Release()
|
||||
for iter.Next() {
|
||||
var cheque Cheque
|
||||
if err := rlp.DecodeBytes(iter.Value(), &cheque); err != nil {
|
||||
continue
|
||||
}
|
||||
cheques = append(cheques, &cheque)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// allIssued returns all issued amount from local address to different
|
||||
// contracts.
|
||||
func (db *chequeDB) allIssued(drawer common.Address) (addresses []common.Address, amounts []*big.Int) {
|
||||
iter := db.db.NewIteratorWithPrefix(append(issuedPrefix, drawer.Bytes()...))
|
||||
defer iter.Release()
|
||||
for iter.Next() {
|
||||
var addr common.Address
|
||||
if len(iter.Key()) != len(issuedPrefix)+common.AddressLength+common.AddressLength {
|
||||
continue
|
||||
}
|
||||
amount := new(big.Int).SetBytes(iter.Value())
|
||||
copy(addr[:], iter.Key()[len(iter.Key())-common.AddressLength:])
|
||||
addresses = append(addresses, addr)
|
||||
amounts = append(amounts, amount)
|
||||
}
|
||||
return
|
||||
}
|
||||
182
contracts/accountbook/chequedb_test.go
Normal file
182
contracts/accountbook/chequedb_test.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accountbook
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func TestPersistContractAddr(t *testing.T) {
|
||||
db := newChequeDB(rawdb.NewMemoryDatabase())
|
||||
local, contract := common.HexToAddress("cafebabe"), common.HexToAddress("deadbeef")
|
||||
|
||||
// Read non-existent data
|
||||
got := db.readContractAddr(local)
|
||||
if got != nil {
|
||||
t.Fatalf("Should return nil for non-existent data")
|
||||
}
|
||||
db.writeContractAddr(local, contract)
|
||||
got = db.readContractAddr(local)
|
||||
if got == nil {
|
||||
t.Fatalf("Can't read back the written addr")
|
||||
}
|
||||
if *got != contract {
|
||||
t.Fatalf("Mismatch between the written addr with read one, want: %s, got %s", contract.Hex(), got.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistCheque(t *testing.T) {
|
||||
db := newChequeDB(rawdb.NewMemoryDatabase())
|
||||
contract := common.HexToAddress("cafebabe")
|
||||
key, _ := crypto.GenerateKey()
|
||||
drawer := crypto.PubkeyToAddress(key.PublicKey)
|
||||
|
||||
// Read non-existent data
|
||||
got := db.readCheque(contract, drawer)
|
||||
if got != nil {
|
||||
t.Fatalf("Should return nil for non-existent data")
|
||||
}
|
||||
cheque := &Cheque{
|
||||
Drawer: drawer,
|
||||
ContractAddr: contract,
|
||||
Amount: big.NewInt(1),
|
||||
}
|
||||
cheque.signWithKey(func(digestHash []byte) ([]byte, error) {
|
||||
sig, _ := crypto.Sign(digestHash, key)
|
||||
return sig, nil
|
||||
})
|
||||
db.writeCheque(contract, drawer, cheque)
|
||||
got = db.readCheque(contract, drawer)
|
||||
if got == nil {
|
||||
t.Fatalf("Failed to retrieve cheque from db")
|
||||
}
|
||||
if !reflect.DeepEqual(cheque, got) {
|
||||
t.Fatalf("Mismatch between the written cheque with the read one")
|
||||
}
|
||||
// Persist a unsigned cheque, it should be retrieved
|
||||
cheque2 := &Cheque{
|
||||
Drawer: drawer,
|
||||
ContractAddr: contract,
|
||||
Amount: big.NewInt(1),
|
||||
}
|
||||
db.writeCheque(contract, drawer, cheque2)
|
||||
got = db.readCheque(contract, drawer)
|
||||
if got == nil {
|
||||
t.Fatalf("Failed to retrieve cheque from db")
|
||||
}
|
||||
if cheque2.Amount.Cmp(got.Amount) != 0 || cheque2.ContractAddr != got.ContractAddr {
|
||||
t.Fatalf("Mismatch between the written cheque with the read one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistLastIssued(t *testing.T) {
|
||||
db := newChequeDB(rawdb.NewMemoryDatabase())
|
||||
drawer, contract := common.HexToAddress("cafebabe"), common.HexToAddress("deadbeef")
|
||||
amount := big.NewInt(100)
|
||||
|
||||
// Read non-existent data
|
||||
got := db.readLastIssued(drawer, contract)
|
||||
if got != nil {
|
||||
t.Fatalf("Should return nil for non-existent data")
|
||||
}
|
||||
db.writeLastIssued(drawer, contract, amount)
|
||||
got = db.readLastIssued(drawer, contract)
|
||||
if got == nil || got.Cmp(amount) != 0 {
|
||||
t.Fatalf("Mismatch between the written amount with the read one, want: %d, got: %d", amount, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListCheques(t *testing.T) {
|
||||
db := newChequeDB(rawdb.NewMemoryDatabase())
|
||||
contract := common.HexToAddress("cafebabe")
|
||||
|
||||
var cheques []*Cheque
|
||||
for i := 0; i < 10; i++ {
|
||||
key, _ := crypto.GenerateKey()
|
||||
drawer := crypto.PubkeyToAddress(key.PublicKey)
|
||||
|
||||
cheque := &Cheque{
|
||||
Drawer: drawer,
|
||||
ContractAddr: contract,
|
||||
Amount: big.NewInt(1),
|
||||
}
|
||||
cheque.signWithKey(func(digestHash []byte) ([]byte, error) {
|
||||
sig, _ := crypto.Sign(digestHash, key)
|
||||
return sig, nil
|
||||
})
|
||||
cheques = append(cheques, cheque)
|
||||
db.writeCheque(contract, drawer, cheque)
|
||||
}
|
||||
got := db.allCheques(contract)
|
||||
if len(got) != len(cheques) {
|
||||
t.Fatalf("Failed to read all cheques")
|
||||
}
|
||||
for _, c1 := range got {
|
||||
var find bool
|
||||
for _, c2 := range cheques {
|
||||
if c1.Drawer == c2.Drawer {
|
||||
find = true
|
||||
if !reflect.DeepEqual(c1, c2) {
|
||||
t.Fatalf("Mismatch between the written cheque with the read one")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !find {
|
||||
t.Fatalf("Miss cheque in the database")
|
||||
}
|
||||
}
|
||||
// Read non-existent records
|
||||
got = db.allCheques(common.HexToAddress("deadbeef"))
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("Should return nil for non-existent data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAllIssued(t *testing.T) {
|
||||
db := newChequeDB(rawdb.NewMemoryDatabase())
|
||||
drawer := common.HexToAddress("cafebabe")
|
||||
|
||||
var (
|
||||
addresses []common.Address
|
||||
amounts []*big.Int
|
||||
)
|
||||
for i := 0; i < 10; i++ {
|
||||
c, amount := common.BytesToAddress([]byte{byte(i + 1)}), big.NewInt(int64(i+1))
|
||||
addresses = append(addresses, c)
|
||||
amounts = append(amounts, amount)
|
||||
db.writeLastIssued(drawer, c, amount)
|
||||
}
|
||||
addresses2, amounts2 := db.allIssued(drawer)
|
||||
if !reflect.DeepEqual(addresses, addresses2) {
|
||||
t.Fatalf("Addresses mismatch, want: %v, got: %v", addresses, addresses2)
|
||||
}
|
||||
if !reflect.DeepEqual(amounts, amounts2) {
|
||||
t.Fatalf("Amounts mismatch, want: %v, got: %v", amounts, amounts2)
|
||||
}
|
||||
// Read non-existent records
|
||||
addresses2, amounts2 = db.allIssued(common.HexToAddress("deadbeef"))
|
||||
if len(addresses2) != 0 || len(amounts2) != 0 {
|
||||
t.Fatalf("Should return nil for non-existent data")
|
||||
}
|
||||
}
|
||||
703
contracts/accountbook/contract/accountbook.go
Normal file
703
contracts/accountbook/contract/accountbook.go
Normal file
|
|
@ -0,0 +1,703 @@
|
|||
// Code generated - DO NOT EDIT.
|
||||
// This file is a generated binding and any manual changes will be lost.
|
||||
|
||||
package contract
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
ethereum "github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var (
|
||||
_ = big.NewInt
|
||||
_ = strings.NewReader
|
||||
_ = ethereum.NotFound
|
||||
_ = abi.U256
|
||||
_ = bind.Bind
|
||||
_ = common.Big1
|
||||
_ = types.BloomLookup
|
||||
_ = event.NewSubscription
|
||||
)
|
||||
|
||||
// AccountBookABI is the input ABI used to generate the binding from.
|
||||
const AccountBookABI = "[{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_challengeTimeWindow\",\"type\":\"uint64\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"oldBalance\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newBalance\",\"type\":\"uint256\"}],\"name\":\"balanceChangedEvent\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdrawEvent\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"internalType\":\"address\",\"name\":\"payer\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"sig_v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"sig_r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"sig_s\",\"type\":\"bytes32\"}],\"name\":\"cash\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"challengeTimeWindow\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"claim\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"deposit\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"deposits\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"addresspayable\",\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"paids\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"withdrawRequests\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"createdAt\",\"type\":\"uint128\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"
|
||||
|
||||
// AccountBookBin is the compiled bytecode used for deploying new contracts.
|
||||
var AccountBookBin = "0x608060405234801561001057600080fd5b506040516108ff3803806108ff8339818101604052602081101561003357600080fd5b5051600380546001600160401b039092167401000000000000000000000000000000000000000002600160a01b600160e01b03196001600160a01b03199093163317929092169190911790556108718061008e6000396000f3fe6080604052600436106100865760003560e01c806352df49ec1161005957806352df49ec146101435780638da5cb5b1461019c578063d0e30db0146101cd578063fbf788d6146101d5578063fc7e286d1461022357610086565b80630c18c9ac1461008b5780632e1a7d4d146100d0578063481acb8f146100fc5780634e71d92d1461012e575b600080fd5b34801561009757600080fd5b506100be600480360360208110156100ae57600080fd5b50356001600160a01b0316610256565b60408051918252519081900360200190f35b3480156100dc57600080fd5b506100fa600480360360208110156100f357600080fd5b5035610268565b005b34801561010857600080fd5b50610111610340565b6040805167ffffffffffffffff9092168252519081900360200190f35b34801561013a57600080fd5b506100fa610357565b34801561014f57600080fd5b506101766004803603602081101561016657600080fd5b50356001600160a01b0316610450565b604080516001600160801b03938416815291909216602082015281519081900390910190f35b3480156101a857600080fd5b506101b1610476565b604080516001600160a01b039092168252519081900360200190f35b6100fa610485565b3480156101e157600080fd5b506100fa600480360360a08110156101f857600080fd5b506001600160a01b038135169060208101359060ff604082013516906060810135906080013561051e565b34801561022f57600080fd5b506100be6004803603602081101561024657600080fd5b50356001600160a01b03166107e9565b60006020819052908152604090205481565b806102725761033d565b3360009081526001602052604090205481111561028e5761033d565b336000908152600260205260409020546001600160801b0316156102b15761033d565b6040805180820182526001600160801b0380841682524381166020808401918252336000818152600283528690209451855493518516600160801b029085166001600160801b031990941693909317909316919091179092558251848152925190927f87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e2692908290030190a25b50565b600354600160a01b900467ffffffffffffffff1681565b336000908152600260205260409020546001600160801b03168061037b575061044e565b60035433600090815260026020526040902054600160a01b90910467ffffffffffffffff16600160801b9091046001600160801b0316430310156103bf575061044e565b33600081815260016020908152604080832080546001600160801b0387168082039092556002909352818420849055905191939281156108fc029290818181858888f19350505050158015610418573d6000803e3d6000fd5b50604080518281526001600160801b038416830360208201528151339260008051602061081d833981519152928290030190a250505b565b6002602052600090815260409020546001600160801b0380821691600160801b90041682565b6003546001600160a01b031681565b3360009081526001602052604090205434810181106104d55760405162461bcd60e51b81526004018080602001828103825260218152602001806107fc6021913960400191505060405180910390fd5b33600081815260016020908152604091829020805434908101909155825185815290850191810191909152815160008051602061081d833981519152929181900390910190a250565b6003546001600160a01b0316331461053557600080fd5b6001600160a01b038516600090815260208190526040902054841161055957600080fd5b60408051601960f81b6020808301919091526000602183018190523060601b6022840152603680840189905284518085039091018152605684018086528151918401919091209190526076830180855281905260ff8716609684015260b6830186905260d68301859052925160019260f68082019392601f1981019281900390910190855afa1580156105f0573d6000803e3d6000fd5b505050602060405103516001600160a01b0316866001600160a01b03161461061757600080fd5b6001600160a01b0386166000908152600160209081526040808320549183905282205490919087038083106106d0576001600160a01b03808a1660009081526001602052604080822084870390819055600354915190955092169183156108fc0291849190818181858888f19350505050158015610699573d6000803e3d6000fd5b50604080518481526020810184905281516001600160a01b038c169260008051602061081d833981519152928290030190a2610755565b8215610755576001600160a01b03808a16600090815260016020526040808220829055600354905192169185156108fc0291869190818181858888f19350505050158015610722573d6000803e3d6000fd5b50604080518481526000602082015281516001600160a01b038c169260008051602061081d833981519152928290030190a25b6001600160a01b0389166000908152602081815260408083208b905560029091529020546001600160801b03168210156107de57816107ac576001600160a01b0389166000908152600260205260408120556107de565b6001600160a01b038916600090815260026020526040902080546001600160801b0319166001600160801b0384161790555b505050505050505050565b6001602052600090815260409020548156fe6164646974696f6e206f766572666c6f77206f72207a65726f206465706f7369744decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6a265627a7a723158207177277e626e0106cb630fb83ae0319188ba7f2a583b1f8846d7c25dbb3daa4164736f6c634300050c0032"
|
||||
|
||||
// DeployAccountBook deploys a new Ethereum contract, binding an instance of AccountBook to it.
|
||||
func DeployAccountBook(auth *bind.TransactOpts, backend bind.ContractBackend, _challengeTimeWindow uint64) (common.Address, *types.Transaction, *AccountBook, error) {
|
||||
parsed, err := abi.JSON(strings.NewReader(AccountBookABI))
|
||||
if err != nil {
|
||||
return common.Address{}, nil, nil, err
|
||||
}
|
||||
|
||||
address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(AccountBookBin), backend, _challengeTimeWindow)
|
||||
if err != nil {
|
||||
return common.Address{}, nil, nil, err
|
||||
}
|
||||
return address, tx, &AccountBook{AccountBookCaller: AccountBookCaller{contract: contract}, AccountBookTransactor: AccountBookTransactor{contract: contract}, AccountBookFilterer: AccountBookFilterer{contract: contract}}, nil
|
||||
}
|
||||
|
||||
// AccountBook is an auto generated Go binding around an Ethereum contract.
|
||||
type AccountBook struct {
|
||||
AccountBookCaller // Read-only binding to the contract
|
||||
AccountBookTransactor // Write-only binding to the contract
|
||||
AccountBookFilterer // Log filterer for contract events
|
||||
}
|
||||
|
||||
// AccountBookCaller is an auto generated read-only Go binding around an Ethereum contract.
|
||||
type AccountBookCaller struct {
|
||||
contract *bind.BoundContract // Generic contract wrapper for the low level calls
|
||||
}
|
||||
|
||||
// AccountBookTransactor is an auto generated write-only Go binding around an Ethereum contract.
|
||||
type AccountBookTransactor struct {
|
||||
contract *bind.BoundContract // Generic contract wrapper for the low level calls
|
||||
}
|
||||
|
||||
// AccountBookFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
|
||||
type AccountBookFilterer struct {
|
||||
contract *bind.BoundContract // Generic contract wrapper for the low level calls
|
||||
}
|
||||
|
||||
// AccountBookSession is an auto generated Go binding around an Ethereum contract,
|
||||
// with pre-set call and transact options.
|
||||
type AccountBookSession struct {
|
||||
Contract *AccountBook // Generic contract binding to set the session for
|
||||
CallOpts bind.CallOpts // Call options to use throughout this session
|
||||
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
|
||||
}
|
||||
|
||||
// AccountBookCallerSession is an auto generated read-only Go binding around an Ethereum contract,
|
||||
// with pre-set call options.
|
||||
type AccountBookCallerSession struct {
|
||||
Contract *AccountBookCaller // Generic contract caller binding to set the session for
|
||||
CallOpts bind.CallOpts // Call options to use throughout this session
|
||||
}
|
||||
|
||||
// AccountBookTransactorSession is an auto generated write-only Go binding around an Ethereum contract,
|
||||
// with pre-set transact options.
|
||||
type AccountBookTransactorSession struct {
|
||||
Contract *AccountBookTransactor // Generic contract transactor binding to set the session for
|
||||
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
|
||||
}
|
||||
|
||||
// AccountBookRaw is an auto generated low-level Go binding around an Ethereum contract.
|
||||
type AccountBookRaw struct {
|
||||
Contract *AccountBook // Generic contract binding to access the raw methods on
|
||||
}
|
||||
|
||||
// AccountBookCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
|
||||
type AccountBookCallerRaw struct {
|
||||
Contract *AccountBookCaller // Generic read-only contract binding to access the raw methods on
|
||||
}
|
||||
|
||||
// AccountBookTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
|
||||
type AccountBookTransactorRaw struct {
|
||||
Contract *AccountBookTransactor // Generic write-only contract binding to access the raw methods on
|
||||
}
|
||||
|
||||
// NewAccountBook creates a new instance of AccountBook, bound to a specific deployed contract.
|
||||
func NewAccountBook(address common.Address, backend bind.ContractBackend) (*AccountBook, error) {
|
||||
contract, err := bindAccountBook(address, backend, backend, backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountBook{AccountBookCaller: AccountBookCaller{contract: contract}, AccountBookTransactor: AccountBookTransactor{contract: contract}, AccountBookFilterer: AccountBookFilterer{contract: contract}}, nil
|
||||
}
|
||||
|
||||
// NewAccountBookCaller creates a new read-only instance of AccountBook, bound to a specific deployed contract.
|
||||
func NewAccountBookCaller(address common.Address, caller bind.ContractCaller) (*AccountBookCaller, error) {
|
||||
contract, err := bindAccountBook(address, caller, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountBookCaller{contract: contract}, nil
|
||||
}
|
||||
|
||||
// NewAccountBookTransactor creates a new write-only instance of AccountBook, bound to a specific deployed contract.
|
||||
func NewAccountBookTransactor(address common.Address, transactor bind.ContractTransactor) (*AccountBookTransactor, error) {
|
||||
contract, err := bindAccountBook(address, nil, transactor, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountBookTransactor{contract: contract}, nil
|
||||
}
|
||||
|
||||
// NewAccountBookFilterer creates a new log filterer instance of AccountBook, bound to a specific deployed contract.
|
||||
func NewAccountBookFilterer(address common.Address, filterer bind.ContractFilterer) (*AccountBookFilterer, error) {
|
||||
contract, err := bindAccountBook(address, nil, nil, filterer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountBookFilterer{contract: contract}, nil
|
||||
}
|
||||
|
||||
// bindAccountBook binds a generic wrapper to an already deployed contract.
|
||||
func bindAccountBook(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) {
|
||||
parsed, err := abi.JSON(strings.NewReader(AccountBookABI))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil
|
||||
}
|
||||
|
||||
// Call invokes the (constant) contract method with params as input values and
|
||||
// sets the output to result. The result type might be a single field for simple
|
||||
// returns, a slice of interfaces for anonymous returns and a struct for named
|
||||
// returns.
|
||||
func (_AccountBook *AccountBookRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error {
|
||||
return _AccountBook.Contract.AccountBookCaller.contract.Call(opts, result, method, params...)
|
||||
}
|
||||
|
||||
// Transfer initiates a plain transaction to move funds to the contract, calling
|
||||
// its default method if one is available.
|
||||
func (_AccountBook *AccountBookRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.AccountBookTransactor.contract.Transfer(opts)
|
||||
}
|
||||
|
||||
// Transact invokes the (paid) contract method with params as input values.
|
||||
func (_AccountBook *AccountBookRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.AccountBookTransactor.contract.Transact(opts, method, params...)
|
||||
}
|
||||
|
||||
// Call invokes the (constant) contract method with params as input values and
|
||||
// sets the output to result. The result type might be a single field for simple
|
||||
// returns, a slice of interfaces for anonymous returns and a struct for named
|
||||
// returns.
|
||||
func (_AccountBook *AccountBookCallerRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error {
|
||||
return _AccountBook.Contract.contract.Call(opts, result, method, params...)
|
||||
}
|
||||
|
||||
// Transfer initiates a plain transaction to move funds to the contract, calling
|
||||
// its default method if one is available.
|
||||
func (_AccountBook *AccountBookTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.contract.Transfer(opts)
|
||||
}
|
||||
|
||||
// Transact invokes the (paid) contract method with params as input values.
|
||||
func (_AccountBook *AccountBookTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.contract.Transact(opts, method, params...)
|
||||
}
|
||||
|
||||
// ChallengeTimeWindow is a free data retrieval call binding the contract method 0x481acb8f.
|
||||
//
|
||||
// Solidity: function challengeTimeWindow() constant returns(uint64)
|
||||
func (_AccountBook *AccountBookCaller) ChallengeTimeWindow(opts *bind.CallOpts) (uint64, error) {
|
||||
var (
|
||||
ret0 = new(uint64)
|
||||
)
|
||||
out := ret0
|
||||
err := _AccountBook.contract.Call(opts, out, "challengeTimeWindow")
|
||||
return *ret0, err
|
||||
}
|
||||
|
||||
// ChallengeTimeWindow is a free data retrieval call binding the contract method 0x481acb8f.
|
||||
//
|
||||
// Solidity: function challengeTimeWindow() constant returns(uint64)
|
||||
func (_AccountBook *AccountBookSession) ChallengeTimeWindow() (uint64, error) {
|
||||
return _AccountBook.Contract.ChallengeTimeWindow(&_AccountBook.CallOpts)
|
||||
}
|
||||
|
||||
// ChallengeTimeWindow is a free data retrieval call binding the contract method 0x481acb8f.
|
||||
//
|
||||
// Solidity: function challengeTimeWindow() constant returns(uint64)
|
||||
func (_AccountBook *AccountBookCallerSession) ChallengeTimeWindow() (uint64, error) {
|
||||
return _AccountBook.Contract.ChallengeTimeWindow(&_AccountBook.CallOpts)
|
||||
}
|
||||
|
||||
// Deposits is a free data retrieval call binding the contract method 0xfc7e286d.
|
||||
//
|
||||
// Solidity: function deposits(address ) constant returns(uint256)
|
||||
func (_AccountBook *AccountBookCaller) Deposits(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) {
|
||||
var (
|
||||
ret0 = new(*big.Int)
|
||||
)
|
||||
out := ret0
|
||||
err := _AccountBook.contract.Call(opts, out, "deposits", arg0)
|
||||
return *ret0, err
|
||||
}
|
||||
|
||||
// Deposits is a free data retrieval call binding the contract method 0xfc7e286d.
|
||||
//
|
||||
// Solidity: function deposits(address ) constant returns(uint256)
|
||||
func (_AccountBook *AccountBookSession) Deposits(arg0 common.Address) (*big.Int, error) {
|
||||
return _AccountBook.Contract.Deposits(&_AccountBook.CallOpts, arg0)
|
||||
}
|
||||
|
||||
// Deposits is a free data retrieval call binding the contract method 0xfc7e286d.
|
||||
//
|
||||
// Solidity: function deposits(address ) constant returns(uint256)
|
||||
func (_AccountBook *AccountBookCallerSession) Deposits(arg0 common.Address) (*big.Int, error) {
|
||||
return _AccountBook.Contract.Deposits(&_AccountBook.CallOpts, arg0)
|
||||
}
|
||||
|
||||
// Owner is a free data retrieval call binding the contract method 0x8da5cb5b.
|
||||
//
|
||||
// Solidity: function owner() constant returns(address)
|
||||
func (_AccountBook *AccountBookCaller) Owner(opts *bind.CallOpts) (common.Address, error) {
|
||||
var (
|
||||
ret0 = new(common.Address)
|
||||
)
|
||||
out := ret0
|
||||
err := _AccountBook.contract.Call(opts, out, "owner")
|
||||
return *ret0, err
|
||||
}
|
||||
|
||||
// Owner is a free data retrieval call binding the contract method 0x8da5cb5b.
|
||||
//
|
||||
// Solidity: function owner() constant returns(address)
|
||||
func (_AccountBook *AccountBookSession) Owner() (common.Address, error) {
|
||||
return _AccountBook.Contract.Owner(&_AccountBook.CallOpts)
|
||||
}
|
||||
|
||||
// Owner is a free data retrieval call binding the contract method 0x8da5cb5b.
|
||||
//
|
||||
// Solidity: function owner() constant returns(address)
|
||||
func (_AccountBook *AccountBookCallerSession) Owner() (common.Address, error) {
|
||||
return _AccountBook.Contract.Owner(&_AccountBook.CallOpts)
|
||||
}
|
||||
|
||||
// Paids is a free data retrieval call binding the contract method 0x0c18c9ac.
|
||||
//
|
||||
// Solidity: function paids(address ) constant returns(uint256)
|
||||
func (_AccountBook *AccountBookCaller) Paids(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) {
|
||||
var (
|
||||
ret0 = new(*big.Int)
|
||||
)
|
||||
out := ret0
|
||||
err := _AccountBook.contract.Call(opts, out, "paids", arg0)
|
||||
return *ret0, err
|
||||
}
|
||||
|
||||
// Paids is a free data retrieval call binding the contract method 0x0c18c9ac.
|
||||
//
|
||||
// Solidity: function paids(address ) constant returns(uint256)
|
||||
func (_AccountBook *AccountBookSession) Paids(arg0 common.Address) (*big.Int, error) {
|
||||
return _AccountBook.Contract.Paids(&_AccountBook.CallOpts, arg0)
|
||||
}
|
||||
|
||||
// Paids is a free data retrieval call binding the contract method 0x0c18c9ac.
|
||||
//
|
||||
// Solidity: function paids(address ) constant returns(uint256)
|
||||
func (_AccountBook *AccountBookCallerSession) Paids(arg0 common.Address) (*big.Int, error) {
|
||||
return _AccountBook.Contract.Paids(&_AccountBook.CallOpts, arg0)
|
||||
}
|
||||
|
||||
// WithdrawRequests is a free data retrieval call binding the contract method 0x52df49ec.
|
||||
//
|
||||
// Solidity: function withdrawRequests(address ) constant returns(uint128 amount, uint128 createdAt)
|
||||
func (_AccountBook *AccountBookCaller) WithdrawRequests(opts *bind.CallOpts, arg0 common.Address) (struct {
|
||||
Amount *big.Int
|
||||
CreatedAt *big.Int
|
||||
}, error) {
|
||||
ret := new(struct {
|
||||
Amount *big.Int
|
||||
CreatedAt *big.Int
|
||||
})
|
||||
out := ret
|
||||
err := _AccountBook.contract.Call(opts, out, "withdrawRequests", arg0)
|
||||
return *ret, err
|
||||
}
|
||||
|
||||
// WithdrawRequests is a free data retrieval call binding the contract method 0x52df49ec.
|
||||
//
|
||||
// Solidity: function withdrawRequests(address ) constant returns(uint128 amount, uint128 createdAt)
|
||||
func (_AccountBook *AccountBookSession) WithdrawRequests(arg0 common.Address) (struct {
|
||||
Amount *big.Int
|
||||
CreatedAt *big.Int
|
||||
}, error) {
|
||||
return _AccountBook.Contract.WithdrawRequests(&_AccountBook.CallOpts, arg0)
|
||||
}
|
||||
|
||||
// WithdrawRequests is a free data retrieval call binding the contract method 0x52df49ec.
|
||||
//
|
||||
// Solidity: function withdrawRequests(address ) constant returns(uint128 amount, uint128 createdAt)
|
||||
func (_AccountBook *AccountBookCallerSession) WithdrawRequests(arg0 common.Address) (struct {
|
||||
Amount *big.Int
|
||||
CreatedAt *big.Int
|
||||
}, error) {
|
||||
return _AccountBook.Contract.WithdrawRequests(&_AccountBook.CallOpts, arg0)
|
||||
}
|
||||
|
||||
// Cash is a paid mutator transaction binding the contract method 0xfbf788d6.
|
||||
//
|
||||
// Solidity: function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) returns()
|
||||
func (_AccountBook *AccountBookTransactor) Cash(opts *bind.TransactOpts, payer common.Address, amount *big.Int, sig_v uint8, sig_r [32]byte, sig_s [32]byte) (*types.Transaction, error) {
|
||||
return _AccountBook.contract.Transact(opts, "cash", payer, amount, sig_v, sig_r, sig_s)
|
||||
}
|
||||
|
||||
// Cash is a paid mutator transaction binding the contract method 0xfbf788d6.
|
||||
//
|
||||
// Solidity: function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) returns()
|
||||
func (_AccountBook *AccountBookSession) Cash(payer common.Address, amount *big.Int, sig_v uint8, sig_r [32]byte, sig_s [32]byte) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Cash(&_AccountBook.TransactOpts, payer, amount, sig_v, sig_r, sig_s)
|
||||
}
|
||||
|
||||
// Cash is a paid mutator transaction binding the contract method 0xfbf788d6.
|
||||
//
|
||||
// Solidity: function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) returns()
|
||||
func (_AccountBook *AccountBookTransactorSession) Cash(payer common.Address, amount *big.Int, sig_v uint8, sig_r [32]byte, sig_s [32]byte) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Cash(&_AccountBook.TransactOpts, payer, amount, sig_v, sig_r, sig_s)
|
||||
}
|
||||
|
||||
// Claim is a paid mutator transaction binding the contract method 0x4e71d92d.
|
||||
//
|
||||
// Solidity: function claim() returns()
|
||||
func (_AccountBook *AccountBookTransactor) Claim(opts *bind.TransactOpts) (*types.Transaction, error) {
|
||||
return _AccountBook.contract.Transact(opts, "claim")
|
||||
}
|
||||
|
||||
// Claim is a paid mutator transaction binding the contract method 0x4e71d92d.
|
||||
//
|
||||
// Solidity: function claim() returns()
|
||||
func (_AccountBook *AccountBookSession) Claim() (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Claim(&_AccountBook.TransactOpts)
|
||||
}
|
||||
|
||||
// Claim is a paid mutator transaction binding the contract method 0x4e71d92d.
|
||||
//
|
||||
// Solidity: function claim() returns()
|
||||
func (_AccountBook *AccountBookTransactorSession) Claim() (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Claim(&_AccountBook.TransactOpts)
|
||||
}
|
||||
|
||||
// Deposit is a paid mutator transaction binding the contract method 0xd0e30db0.
|
||||
//
|
||||
// Solidity: function deposit() returns()
|
||||
func (_AccountBook *AccountBookTransactor) Deposit(opts *bind.TransactOpts) (*types.Transaction, error) {
|
||||
return _AccountBook.contract.Transact(opts, "deposit")
|
||||
}
|
||||
|
||||
// Deposit is a paid mutator transaction binding the contract method 0xd0e30db0.
|
||||
//
|
||||
// Solidity: function deposit() returns()
|
||||
func (_AccountBook *AccountBookSession) Deposit() (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Deposit(&_AccountBook.TransactOpts)
|
||||
}
|
||||
|
||||
// Deposit is a paid mutator transaction binding the contract method 0xd0e30db0.
|
||||
//
|
||||
// Solidity: function deposit() returns()
|
||||
func (_AccountBook *AccountBookTransactorSession) Deposit() (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Deposit(&_AccountBook.TransactOpts)
|
||||
}
|
||||
|
||||
// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d.
|
||||
//
|
||||
// Solidity: function withdraw(uint256 amount) returns()
|
||||
func (_AccountBook *AccountBookTransactor) Withdraw(opts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) {
|
||||
return _AccountBook.contract.Transact(opts, "withdraw", amount)
|
||||
}
|
||||
|
||||
// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d.
|
||||
//
|
||||
// Solidity: function withdraw(uint256 amount) returns()
|
||||
func (_AccountBook *AccountBookSession) Withdraw(amount *big.Int) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Withdraw(&_AccountBook.TransactOpts, amount)
|
||||
}
|
||||
|
||||
// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d.
|
||||
//
|
||||
// Solidity: function withdraw(uint256 amount) returns()
|
||||
func (_AccountBook *AccountBookTransactorSession) Withdraw(amount *big.Int) (*types.Transaction, error) {
|
||||
return _AccountBook.Contract.Withdraw(&_AccountBook.TransactOpts, amount)
|
||||
}
|
||||
|
||||
// AccountBookBalanceChangedEventIterator is returned from FilterBalanceChangedEvent and is used to iterate over the raw logs and unpacked data for BalanceChangedEvent events raised by the AccountBook contract.
|
||||
type AccountBookBalanceChangedEventIterator struct {
|
||||
Event *AccountBookBalanceChangedEvent // Event containing the contract specifics and raw log
|
||||
|
||||
contract *bind.BoundContract // Generic contract to use for unpacking event data
|
||||
event string // Event name to use for unpacking event data
|
||||
|
||||
logs chan types.Log // Log channel receiving the found contract events
|
||||
sub ethereum.Subscription // Subscription for errors, completion and termination
|
||||
done bool // Whether the subscription completed delivering logs
|
||||
fail error // Occurred error to stop iteration
|
||||
}
|
||||
|
||||
// Next advances the iterator to the subsequent event, returning whether there
|
||||
// are any more events found. In case of a retrieval or parsing error, false is
|
||||
// returned and Error() can be queried for the exact failure.
|
||||
func (it *AccountBookBalanceChangedEventIterator) Next() bool {
|
||||
// If the iterator failed, stop iterating
|
||||
if it.fail != nil {
|
||||
return false
|
||||
}
|
||||
// If the iterator completed, deliver directly whatever's available
|
||||
if it.done {
|
||||
select {
|
||||
case log := <-it.logs:
|
||||
it.Event = new(AccountBookBalanceChangedEvent)
|
||||
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
|
||||
it.fail = err
|
||||
return false
|
||||
}
|
||||
it.Event.Raw = log
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Iterator still in progress, wait for either a data or an error event
|
||||
select {
|
||||
case log := <-it.logs:
|
||||
it.Event = new(AccountBookBalanceChangedEvent)
|
||||
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
|
||||
it.fail = err
|
||||
return false
|
||||
}
|
||||
it.Event.Raw = log
|
||||
return true
|
||||
|
||||
case err := <-it.sub.Err():
|
||||
it.done = true
|
||||
it.fail = err
|
||||
return it.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns any retrieval or parsing error occurred during filtering.
|
||||
func (it *AccountBookBalanceChangedEventIterator) Error() error {
|
||||
return it.fail
|
||||
}
|
||||
|
||||
// Close terminates the iteration process, releasing any pending underlying
|
||||
// resources.
|
||||
func (it *AccountBookBalanceChangedEventIterator) Close() error {
|
||||
it.sub.Unsubscribe()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AccountBookBalanceChangedEvent represents a BalanceChangedEvent event raised by the AccountBook contract.
|
||||
type AccountBookBalanceChangedEvent struct {
|
||||
Addr common.Address
|
||||
OldBalance *big.Int
|
||||
NewBalance *big.Int
|
||||
Raw types.Log // Blockchain specific contextual infos
|
||||
}
|
||||
|
||||
// FilterBalanceChangedEvent is a free log retrieval operation binding the contract event 0x4decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6.
|
||||
//
|
||||
// Solidity: event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance)
|
||||
func (_AccountBook *AccountBookFilterer) FilterBalanceChangedEvent(opts *bind.FilterOpts, addr []common.Address) (*AccountBookBalanceChangedEventIterator, error) {
|
||||
|
||||
var addrRule []interface{}
|
||||
for _, addrItem := range addr {
|
||||
addrRule = append(addrRule, addrItem)
|
||||
}
|
||||
|
||||
logs, sub, err := _AccountBook.contract.FilterLogs(opts, "balanceChangedEvent", addrRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountBookBalanceChangedEventIterator{contract: _AccountBook.contract, event: "balanceChangedEvent", logs: logs, sub: sub}, nil
|
||||
}
|
||||
|
||||
// WatchBalanceChangedEvent is a free log subscription operation binding the contract event 0x4decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6.
|
||||
//
|
||||
// Solidity: event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance)
|
||||
func (_AccountBook *AccountBookFilterer) WatchBalanceChangedEvent(opts *bind.WatchOpts, sink chan<- *AccountBookBalanceChangedEvent, addr []common.Address) (event.Subscription, error) {
|
||||
|
||||
var addrRule []interface{}
|
||||
for _, addrItem := range addr {
|
||||
addrRule = append(addrRule, addrItem)
|
||||
}
|
||||
|
||||
logs, sub, err := _AccountBook.contract.WatchLogs(opts, "balanceChangedEvent", addrRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event.NewSubscription(func(quit <-chan struct{}) error {
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case log := <-logs:
|
||||
// New log arrived, parse the event and forward to the user
|
||||
event := new(AccountBookBalanceChangedEvent)
|
||||
if err := _AccountBook.contract.UnpackLog(event, "balanceChangedEvent", log); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Raw = log
|
||||
|
||||
select {
|
||||
case sink <- event:
|
||||
case err := <-sub.Err():
|
||||
return err
|
||||
case <-quit:
|
||||
return nil
|
||||
}
|
||||
case err := <-sub.Err():
|
||||
return err
|
||||
case <-quit:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
// ParseBalanceChangedEvent is a log parse operation binding the contract event 0x4decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6.
|
||||
//
|
||||
// Solidity: event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance)
|
||||
func (_AccountBook *AccountBookFilterer) ParseBalanceChangedEvent(log types.Log) (*AccountBookBalanceChangedEvent, error) {
|
||||
event := new(AccountBookBalanceChangedEvent)
|
||||
if err := _AccountBook.contract.UnpackLog(event, "balanceChangedEvent", log); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// AccountBookWithdrawEventIterator is returned from FilterWithdrawEvent and is used to iterate over the raw logs and unpacked data for WithdrawEvent events raised by the AccountBook contract.
|
||||
type AccountBookWithdrawEventIterator struct {
|
||||
Event *AccountBookWithdrawEvent // Event containing the contract specifics and raw log
|
||||
|
||||
contract *bind.BoundContract // Generic contract to use for unpacking event data
|
||||
event string // Event name to use for unpacking event data
|
||||
|
||||
logs chan types.Log // Log channel receiving the found contract events
|
||||
sub ethereum.Subscription // Subscription for errors, completion and termination
|
||||
done bool // Whether the subscription completed delivering logs
|
||||
fail error // Occurred error to stop iteration
|
||||
}
|
||||
|
||||
// Next advances the iterator to the subsequent event, returning whether there
|
||||
// are any more events found. In case of a retrieval or parsing error, false is
|
||||
// returned and Error() can be queried for the exact failure.
|
||||
func (it *AccountBookWithdrawEventIterator) Next() bool {
|
||||
// If the iterator failed, stop iterating
|
||||
if it.fail != nil {
|
||||
return false
|
||||
}
|
||||
// If the iterator completed, deliver directly whatever's available
|
||||
if it.done {
|
||||
select {
|
||||
case log := <-it.logs:
|
||||
it.Event = new(AccountBookWithdrawEvent)
|
||||
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
|
||||
it.fail = err
|
||||
return false
|
||||
}
|
||||
it.Event.Raw = log
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Iterator still in progress, wait for either a data or an error event
|
||||
select {
|
||||
case log := <-it.logs:
|
||||
it.Event = new(AccountBookWithdrawEvent)
|
||||
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
|
||||
it.fail = err
|
||||
return false
|
||||
}
|
||||
it.Event.Raw = log
|
||||
return true
|
||||
|
||||
case err := <-it.sub.Err():
|
||||
it.done = true
|
||||
it.fail = err
|
||||
return it.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns any retrieval or parsing error occurred during filtering.
|
||||
func (it *AccountBookWithdrawEventIterator) Error() error {
|
||||
return it.fail
|
||||
}
|
||||
|
||||
// Close terminates the iteration process, releasing any pending underlying
|
||||
// resources.
|
||||
func (it *AccountBookWithdrawEventIterator) Close() error {
|
||||
it.sub.Unsubscribe()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AccountBookWithdrawEvent represents a WithdrawEvent event raised by the AccountBook contract.
|
||||
type AccountBookWithdrawEvent struct {
|
||||
Addr common.Address
|
||||
Amount *big.Int
|
||||
Raw types.Log // Blockchain specific contextual infos
|
||||
}
|
||||
|
||||
// FilterWithdrawEvent is a free log retrieval operation binding the contract event 0x87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e26.
|
||||
//
|
||||
// Solidity: event withdrawEvent(address indexed addr, uint256 amount)
|
||||
func (_AccountBook *AccountBookFilterer) FilterWithdrawEvent(opts *bind.FilterOpts, addr []common.Address) (*AccountBookWithdrawEventIterator, error) {
|
||||
|
||||
var addrRule []interface{}
|
||||
for _, addrItem := range addr {
|
||||
addrRule = append(addrRule, addrItem)
|
||||
}
|
||||
|
||||
logs, sub, err := _AccountBook.contract.FilterLogs(opts, "withdrawEvent", addrRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountBookWithdrawEventIterator{contract: _AccountBook.contract, event: "withdrawEvent", logs: logs, sub: sub}, nil
|
||||
}
|
||||
|
||||
// WatchWithdrawEvent is a free log subscription operation binding the contract event 0x87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e26.
|
||||
//
|
||||
// Solidity: event withdrawEvent(address indexed addr, uint256 amount)
|
||||
func (_AccountBook *AccountBookFilterer) WatchWithdrawEvent(opts *bind.WatchOpts, sink chan<- *AccountBookWithdrawEvent, addr []common.Address) (event.Subscription, error) {
|
||||
|
||||
var addrRule []interface{}
|
||||
for _, addrItem := range addr {
|
||||
addrRule = append(addrRule, addrItem)
|
||||
}
|
||||
|
||||
logs, sub, err := _AccountBook.contract.WatchLogs(opts, "withdrawEvent", addrRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event.NewSubscription(func(quit <-chan struct{}) error {
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case log := <-logs:
|
||||
// New log arrived, parse the event and forward to the user
|
||||
event := new(AccountBookWithdrawEvent)
|
||||
if err := _AccountBook.contract.UnpackLog(event, "withdrawEvent", log); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Raw = log
|
||||
|
||||
select {
|
||||
case sink <- event:
|
||||
case err := <-sub.Err():
|
||||
return err
|
||||
case <-quit:
|
||||
return nil
|
||||
}
|
||||
case err := <-sub.Err():
|
||||
return err
|
||||
case <-quit:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
// ParseWithdrawEvent is a log parse operation binding the contract event 0x87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e26.
|
||||
//
|
||||
// Solidity: event withdrawEvent(address indexed addr, uint256 amount)
|
||||
func (_AccountBook *AccountBookFilterer) ParseWithdrawEvent(log types.Log) (*AccountBookWithdrawEvent, error) {
|
||||
event := new(AccountBookWithdrawEvent)
|
||||
if err := _AccountBook.contract.UnpackLog(event, "withdrawEvent", log); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
212
contracts/accountbook/contract/accountbook.sol
Normal file
212
contracts/accountbook/contract/accountbook.sol
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
pragma solidity ^0.5.12;
|
||||
|
||||
/**
|
||||
* @title AccountBook
|
||||
* @author Gary Rong<garyrong@ethereum.org>
|
||||
* @dev Implementation of the account book which les server can use to
|
||||
* @dev process micropayments from customers.
|
||||
*/
|
||||
contract AccountBook {
|
||||
/*
|
||||
Events
|
||||
*/
|
||||
|
||||
// withdrawEvent is emitted if customer opens a request to withdraw
|
||||
// deposit.
|
||||
event withdrawEvent(address indexed addr, uint256 amount);
|
||||
|
||||
// balanceChangedEvent is emitted when the deposit balance of customer
|
||||
// is changed.
|
||||
event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance);
|
||||
|
||||
/*
|
||||
Definitions
|
||||
*/
|
||||
|
||||
// withdrawRequest defines all necessary fields of
|
||||
// the withdraw request from customers
|
||||
struct withdrawRequest {
|
||||
uint128 amount; // the amount requested to withdraw
|
||||
uint128 createdAt; // the created block number
|
||||
}
|
||||
|
||||
/*
|
||||
Modifier
|
||||
*/
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner);
|
||||
_;
|
||||
}
|
||||
|
||||
/*
|
||||
Public Functions
|
||||
*/
|
||||
constructor(uint64 _challengeTimeWindow) public {
|
||||
owner = msg.sender;
|
||||
|
||||
// challengeTimeWindow is decided by owner itself. In theory
|
||||
// the longer challenge time window, the safer for owner. But
|
||||
// in the mean time, it will take longer for customers to withdraw
|
||||
// money. The inconvenience may lead to lose potentional customers.
|
||||
challengeTimeWindow = _challengeTimeWindow;
|
||||
}
|
||||
|
||||
// deposit adds the amount of money into the account book
|
||||
function deposit() payable public {
|
||||
uint256 balance = deposits[msg.sender];
|
||||
require(balance + msg.value > balance, "addition overflow or zero deposit");
|
||||
deposits[msg.sender] += msg.value;
|
||||
|
||||
emit balanceChangedEvent(msg.sender, balance, balance+msg.value);
|
||||
}
|
||||
|
||||
// withdraw opens a request to withdraw the deposit from the
|
||||
// account book. Caller has to wait challengeTimeWindow blocks
|
||||
// time which leaves enough time window for owner to challenge
|
||||
// the withdraw amount.
|
||||
// @amount: the amount of deposit to withdraw
|
||||
function withdraw(uint256 amount) public {
|
||||
// Ensure it's a meaningful withdraw request
|
||||
if (amount == 0) {
|
||||
return;
|
||||
}
|
||||
// Ensure the customer has enough deposit to withdraw
|
||||
if (deposits[msg.sender] < amount) {
|
||||
return;
|
||||
}
|
||||
// Account book can only process one withdraw request
|
||||
// at the same time.
|
||||
if (withdrawRequests[msg.sender].amount > 0) {
|
||||
return;
|
||||
}
|
||||
// Convert the amount and block number into uin128 so that
|
||||
// only 1 slot is necessary(we can save 20,000 gas cost).
|
||||
withdrawRequests[msg.sender] = withdrawRequest({amount: uint128(amount), createdAt: uint128(block.number)});
|
||||
emit withdrawEvent(msg.sender, amount);
|
||||
}
|
||||
|
||||
// claim withdraws the deposit from the account book which
|
||||
// has passed the challenge period.
|
||||
function claim() public {
|
||||
uint128 amount = withdrawRequests[msg.sender].amount;
|
||||
|
||||
// Short circuit if the withdrawal amount is zero.
|
||||
// There are several situations can lead to this case:
|
||||
// * there is no withdrawal request at all
|
||||
// * there is no withdrawable deposit since all of the deposit
|
||||
// is used and cashed by owner of the account book
|
||||
if (amount == 0) {
|
||||
return;
|
||||
}
|
||||
// Ensure the request has passed the challenge period.
|
||||
if (block.number - withdrawRequests[msg.sender].createdAt < challengeTimeWindow) {
|
||||
return;
|
||||
}
|
||||
// Decrease the balance of customer before transfer.
|
||||
uint256 balance = deposits[msg.sender];
|
||||
deposits[msg.sender] -= amount;
|
||||
delete withdrawRequests[msg.sender]; // Release the withdraw lock.
|
||||
msg.sender.transfer(amount);
|
||||
emit balanceChangedEvent(msg.sender, balance, balance-amount);
|
||||
}
|
||||
|
||||
// cash claims the specified amount of money from payer's deposit with
|
||||
// offchain signature.
|
||||
//
|
||||
// For cash operation, since it's called by owner itself, so that it's
|
||||
// unnecessary to leave a challenge time window.
|
||||
//
|
||||
// This function can be called in two cases:
|
||||
// * the owner of account book thinks there are too many payments made
|
||||
// by customers
|
||||
// * if the customer opens a withdraw request which tries to withdraw
|
||||
// some spent money, it's a way to challenge the request.
|
||||
//
|
||||
// @payer: the address of payer who has made a few micropayments off-chain.
|
||||
// @amount : the amount of money owner wants to cash
|
||||
// @sig_v : the v-value of the signature
|
||||
// @sig_r : the r-value of the signature
|
||||
// @sig_s : the s-value of the signature
|
||||
function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) onlyOwner public {
|
||||
// In order to prevent the owner to double-cash the
|
||||
// cheque of customer, we record the cashed amount
|
||||
// in contract. Only higher signed amount can make
|
||||
// a valid cash operation.
|
||||
require(amount > paids[payer]);
|
||||
|
||||
// Check the digital signature of the cheque.
|
||||
//
|
||||
// EIP 191 style signatures
|
||||
//
|
||||
// Arguments when calculating hash to validate
|
||||
// 1: byte(0x19) - the initial 0x19 byte
|
||||
// 2: byte(0) - the version byte (data with intended validator)
|
||||
// 3: this - the validator address
|
||||
// -- Application specific data
|
||||
// 4: amount the amount of paid money
|
||||
// 5: chainID(todo need istanbul fork)
|
||||
bytes32 hash = keccak256(abi.encodePacked(byte(0x19), byte(0), this, amount));
|
||||
require(payer == ecrecover(hash, sig_v, sig_r, sig_s));
|
||||
|
||||
// Cash all cheques to owner's account directly.
|
||||
uint256 balance = deposits[payer];
|
||||
uint256 newBalance;
|
||||
uint256 diff = amount - paids[payer];
|
||||
|
||||
// Move the money into owner's address
|
||||
if (balance >= diff) {
|
||||
// Payer has enough deposit to cover all spends
|
||||
newBalance = balance - diff;
|
||||
deposits[payer] = newBalance;
|
||||
owner.transfer(diff);
|
||||
emit balanceChangedEvent(payer, balance, newBalance);
|
||||
} else if (balance > 0) {
|
||||
// It can happen that payer doesn't have enough deposit to cover spends.
|
||||
// In theory owner should reject all "invalid" cheques off-chain. But if
|
||||
// some errors occur that owner accept the "useless" cheque, we still support
|
||||
// owner to cash all "spent" money.
|
||||
delete deposits[payer];
|
||||
|
||||
// Transfer all remaing money into owner's pocket.
|
||||
owner.transfer(balance);
|
||||
emit balanceChangedEvent(payer, balance, 0);
|
||||
}
|
||||
paids[payer] = amount; // Record all cashed amount in order to prevent "double-cash"
|
||||
|
||||
// If customer want to withdraw some spent money, reject
|
||||
// it by decreaing the amount or just delete the request.
|
||||
// It's the challenge action for invalid withdraw request.
|
||||
if (withdrawRequests[payer].amount > newBalance) {
|
||||
if (newBalance == 0) {
|
||||
delete withdrawRequests[payer];
|
||||
} else {
|
||||
withdrawRequests[payer].amount = uint128(newBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Fields
|
||||
*/
|
||||
// paids is the map which contains the cumulative paid amount
|
||||
// in wei from each customer
|
||||
mapping(address => uint256) public paids;
|
||||
|
||||
// deposits is the map which contains the deposit of customers.
|
||||
// Customers can withdraw unused deposit back.
|
||||
mapping(address => uint256) public deposits;
|
||||
|
||||
// withdrawRequests is the map which contains all withdraw
|
||||
// requests from customers, no matter to withdraw all deposit
|
||||
// or a part.
|
||||
mapping(address => withdrawRequest) public withdrawRequests;
|
||||
|
||||
// owner is the address of the account book owner(the address
|
||||
// of les server).
|
||||
address payable public owner;
|
||||
|
||||
// challengeTimeWindow is the maximum time that owner can perform
|
||||
// challenge when customer requests deposit withdrawal. It's count
|
||||
// by block number.
|
||||
uint64 public challengeTimeWindow;
|
||||
}
|
||||
465
contracts/accountbook/contract/accountbook_test.go
Normal file
465
contracts/accountbook/contract/accountbook_test.go
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package contract
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
var (
|
||||
testChallengeTimeWindow = big.NewInt(4)
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
key *ecdsa.PrivateKey
|
||||
addr common.Address
|
||||
}
|
||||
|
||||
type testAccountBook struct {
|
||||
sim *backends.SimulatedBackend
|
||||
contract *AccountBook
|
||||
address common.Address
|
||||
owner Account
|
||||
customer Account
|
||||
}
|
||||
|
||||
func newAccount() Account {
|
||||
key, _ := crypto.GenerateKey()
|
||||
return Account{addr: crypto.PubkeyToAddress(key.PublicKey), key: key}
|
||||
}
|
||||
|
||||
func newTestContract(t *testing.T) *testAccountBook {
|
||||
owner, customer := newAccount(), newAccount()
|
||||
|
||||
sim := backends.NewSimulatedBackend(core.GenesisAlloc{owner.addr: {Balance: big.NewInt(1000000000)}, customer.addr: {Balance: big.NewInt(1000000000)}}, 10000000)
|
||||
transactOpts := bind.NewKeyedTransactor(owner.key)
|
||||
|
||||
addr, _, c, err := DeployAccountBook(transactOpts, sim, testChallengeTimeWindow.Uint64())
|
||||
if err != nil {
|
||||
t.Error("Failed to deploy registrar contract", err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
return &testAccountBook{
|
||||
sim: sim,
|
||||
contract: c,
|
||||
address: addr,
|
||||
owner: owner,
|
||||
customer: customer,
|
||||
}
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) teardown() {
|
||||
tester.sim.Close()
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) listenBalanceChangeEvent(res chan bool, callback func(new *big.Int, old *big.Int) bool) {
|
||||
sink := make(chan *AccountBookBalanceChangedEvent)
|
||||
sub, err := tester.contract.WatchBalanceChangedEvent(nil, sink, nil)
|
||||
if err != nil {
|
||||
res <- false
|
||||
return
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Check whether we receive the desired event
|
||||
select {
|
||||
case ev := <-sink:
|
||||
if !callback(ev.NewBalance, ev.OldBalance) {
|
||||
res <- false
|
||||
return
|
||||
}
|
||||
case <-time.NewTimer(time.Second).C:
|
||||
res <- false // Timeout
|
||||
return
|
||||
}
|
||||
// Ensure no more additional event receive
|
||||
select {
|
||||
case <-sink:
|
||||
res <- false
|
||||
return
|
||||
case <-time.NewTimer(100 * time.Millisecond).C:
|
||||
res <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) listenWithdrawEvent(res chan bool, callback func(addr common.Address, amount *big.Int) bool) {
|
||||
sink := make(chan *AccountBookWithdrawEvent)
|
||||
sub, err := tester.contract.WatchWithdrawEvent(nil, sink, nil)
|
||||
if err != nil {
|
||||
res <- false
|
||||
return
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Check whether we receive the desired event
|
||||
select {
|
||||
case ev := <-sink:
|
||||
if !callback(ev.Addr, ev.Amount) {
|
||||
res <- false
|
||||
return
|
||||
}
|
||||
case <-time.NewTimer(time.Second).C:
|
||||
res <- false // Timeout
|
||||
return
|
||||
}
|
||||
// Ensure no more additional event receive
|
||||
select {
|
||||
case <-sink:
|
||||
res <- false
|
||||
return
|
||||
case <-time.NewTimer(100 * time.Millisecond).C:
|
||||
res <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) issueCheque(amount *big.Int) []byte {
|
||||
buf := make([]byte, 32)
|
||||
copy(buf[32-len(amount.Bytes()):], amount.Bytes())
|
||||
data := append([]byte{0x19, 0x00}, append(tester.address.Bytes(), buf...)...)
|
||||
sig, _ := crypto.Sign(crypto.Keccak256(data), tester.customer.key)
|
||||
sig[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||
return sig
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) commitEmptyBlocks(number int) {
|
||||
for i := 0; i < number; i++ {
|
||||
tester.sim.Commit()
|
||||
}
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) balance(address common.Address) *big.Int {
|
||||
balance, err := tester.sim.BalanceAt(context.Background(), address, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return balance
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) contractBalance() *big.Int {
|
||||
return tester.balance(tester.address)
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) ownerBalance() *big.Int {
|
||||
return tester.balance(tester.owner.addr)
|
||||
}
|
||||
|
||||
func (tester *testAccountBook) customerBalance() *big.Int {
|
||||
return tester.balance(tester.customer.addr)
|
||||
}
|
||||
|
||||
func TestDeposit(t *testing.T) {
|
||||
tester := newTestContract(t)
|
||||
defer tester.teardown()
|
||||
|
||||
eventCh := make(chan bool, 1)
|
||||
go tester.listenBalanceChangeEvent(eventCh, func(new *big.Int, old *big.Int) bool {
|
||||
return new.Cmp(big.NewInt(10000)) == 0 && old.Cmp(big.NewInt(0)) == 0
|
||||
})
|
||||
|
||||
// Deposit 10,000 wei
|
||||
opt := bind.NewKeyedTransactor(tester.customer.key)
|
||||
opt.Value = big.NewInt(10000)
|
||||
tester.contract.Deposit(opt)
|
||||
tester.sim.Commit()
|
||||
|
||||
balance, err := tester.contract.Deposits(nil, tester.customer.addr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve balanace: %v", err)
|
||||
}
|
||||
want := big.NewInt(10000)
|
||||
if balance.Cmp(want) != 0 {
|
||||
t.Fatalf("Balance mismtach, want: %d, got: %d", want, balance)
|
||||
}
|
||||
if !<-eventCh {
|
||||
t.Fatalf("Failed for balance change event")
|
||||
}
|
||||
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
|
||||
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
|
||||
}
|
||||
|
||||
// Deposit 0 wei, we don't accept empty deposit
|
||||
opt.Value = nil
|
||||
_, err = tester.contract.Deposit(opt)
|
||||
if err == nil {
|
||||
t.Fatalf("Zero deposit should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdraw(t *testing.T) {
|
||||
tester := newTestContract(t)
|
||||
defer tester.teardown()
|
||||
|
||||
// Deposit 10,000 wei
|
||||
opt := bind.NewKeyedTransactor(tester.customer.key)
|
||||
opt.Value = big.NewInt(10000)
|
||||
tester.contract.Deposit(opt)
|
||||
tester.sim.Commit()
|
||||
|
||||
eventCh := make(chan bool, 1)
|
||||
go tester.listenWithdrawEvent(eventCh, func(addr common.Address, amount *big.Int) bool {
|
||||
return addr == tester.customer.addr && amount.Cmp(big.NewInt(10000)) == 0
|
||||
})
|
||||
|
||||
// Open the request for withdrawal
|
||||
opt.Value = nil
|
||||
tester.contract.Withdraw(opt, big.NewInt(10000)) // Withdraw all money
|
||||
tester.sim.Commit()
|
||||
if !<-eventCh {
|
||||
t.Fatalf("Failed for withdraw event")
|
||||
}
|
||||
|
||||
// Check there is a open withdrawal request
|
||||
request, err := tester.contract.WithdrawRequests(nil, tester.customer.addr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve withdraw request: %v", err)
|
||||
}
|
||||
if request.Amount.Cmp(big.NewInt(10000)) != 0 {
|
||||
t.Fatalf("Withdrawal amount mismatch, want: %d, got: %d", 10000, request.Amount)
|
||||
}
|
||||
|
||||
// Try to claim the deposit before challenge
|
||||
tester.contract.Claim(opt)
|
||||
tester.sim.Commit()
|
||||
balance, err := tester.contract.Deposits(nil, tester.customer.addr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve balanace: %v", err)
|
||||
}
|
||||
if balance.Cmp(big.NewInt(10000)) != 0 {
|
||||
t.Fatalf("Deposit can't be withdraw during the challenge period")
|
||||
}
|
||||
|
||||
// Pass the challenge period and withdraw
|
||||
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
|
||||
|
||||
go tester.listenBalanceChangeEvent(eventCh, func(new *big.Int, old *big.Int) bool {
|
||||
return old.Cmp(big.NewInt(10000)) == 0 && new.Cmp(big.NewInt(0)) == 0
|
||||
})
|
||||
tester.contract.Claim(opt)
|
||||
tester.sim.Commit()
|
||||
|
||||
balance, err = tester.contract.Deposits(nil, tester.customer.addr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve balanace: %v", err)
|
||||
}
|
||||
want := big.NewInt(0)
|
||||
if balance.Cmp(want) != 0 {
|
||||
t.Fatalf("Balance mismtach, want: %d, got: %d", want, balance)
|
||||
}
|
||||
if !<-eventCh {
|
||||
t.Fatalf("Failed for balance change event")
|
||||
}
|
||||
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
|
||||
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCash(t *testing.T) {
|
||||
tester := newTestContract(t)
|
||||
defer tester.teardown()
|
||||
|
||||
// Deposit 10,000 wei
|
||||
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
|
||||
customerOpt.Value = big.NewInt(10000)
|
||||
tester.contract.Deposit(customerOpt)
|
||||
tester.sim.Commit()
|
||||
|
||||
// Customer issues a cheque with amount 1000
|
||||
sig := tester.issueCheque(big.NewInt(1000))
|
||||
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
|
||||
_, err := tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to cash the signed cheque: %v", err)
|
||||
}
|
||||
tester.sim.Commit()
|
||||
balance, err := tester.contract.Deposits(nil, tester.customer.addr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve balanace: %v", err)
|
||||
}
|
||||
want := big.NewInt(9000)
|
||||
if balance.Cmp(want) != 0 {
|
||||
t.Fatalf("Balance mismtach, want: %d, got: %d", want, balance)
|
||||
}
|
||||
// The stored money in contract should also changed
|
||||
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
|
||||
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
|
||||
}
|
||||
|
||||
// Try to double-cash, prevent it.
|
||||
_, err = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
|
||||
if err == nil {
|
||||
t.Fatalf("Double-cash should be prevent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallenge(t *testing.T) {
|
||||
tester := newTestContract(t)
|
||||
defer tester.teardown()
|
||||
|
||||
// Deposit 10,000 wei
|
||||
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
|
||||
customerOpt.Value = big.NewInt(10000)
|
||||
tester.contract.Deposit(customerOpt)
|
||||
tester.sim.Commit()
|
||||
|
||||
// Customer issues a cheque with amount 1000
|
||||
sig := tester.issueCheque(big.NewInt(1000))
|
||||
|
||||
// Customer tries to withdraw all money including the spent part
|
||||
customerOpt.Value = nil
|
||||
_, err := tester.contract.Withdraw(customerOpt, big.NewInt(10000))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open withdraw request: %v", err)
|
||||
}
|
||||
tester.sim.Commit()
|
||||
|
||||
// Owner sumbits the evidence to cash the "spent" money
|
||||
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
|
||||
_, err = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to cash the signed cheque: %v", err)
|
||||
}
|
||||
tester.sim.Commit()
|
||||
|
||||
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
|
||||
|
||||
eventCh := make(chan bool, 1)
|
||||
go tester.listenBalanceChangeEvent(eventCh, func(new *big.Int, old *big.Int) bool {
|
||||
return old.Cmp(big.NewInt(9000)) == 0 && new.Cmp(big.NewInt(0)) == 0
|
||||
})
|
||||
tester.contract.Claim(customerOpt)
|
||||
tester.sim.Commit()
|
||||
if !<-eventCh {
|
||||
t.Fatalf("Failed for balance change event")
|
||||
}
|
||||
|
||||
want := big.NewInt(0)
|
||||
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
|
||||
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallengeOutOfWindow(t *testing.T) {
|
||||
tester := newTestContract(t)
|
||||
defer tester.teardown()
|
||||
|
||||
// Deposit 10,000 wei
|
||||
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
|
||||
customerOpt.Value = big.NewInt(10000)
|
||||
tester.contract.Deposit(customerOpt)
|
||||
tester.sim.Commit()
|
||||
|
||||
// Customer issues a cheque with amount 1000
|
||||
sig := tester.issueCheque(big.NewInt(1000))
|
||||
|
||||
// Customer tries to withdraw all money including the spent part
|
||||
customerOpt.Value = nil
|
||||
_, err := tester.contract.Withdraw(customerOpt, big.NewInt(10000))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open withdraw request: %v", err)
|
||||
}
|
||||
tester.sim.Commit()
|
||||
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
|
||||
|
||||
// Now all the money has been claimed.
|
||||
tester.contract.Claim(customerOpt)
|
||||
tester.sim.Commit()
|
||||
|
||||
// Owner sumbits the evidence to cash the "spent" money, but it's
|
||||
// too late.
|
||||
ownerBalanceOld := tester.ownerBalance()
|
||||
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
|
||||
ownerOpt.GasPrice = big.NewInt(0)
|
||||
_, err = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to cash the signed cheque: %v", err)
|
||||
}
|
||||
tester.sim.Commit()
|
||||
|
||||
ownerBalanceNew := tester.ownerBalance()
|
||||
if ownerBalanceNew.Cmp(ownerBalanceOld) != 0 {
|
||||
t.Fatalf("All claimed money should be cashed")
|
||||
}
|
||||
paid, err := tester.contract.Paids(nil, tester.customer.addr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve paid amount: %v", err)
|
||||
}
|
||||
if paid.Cmp(big.NewInt(1000)) != 0 {
|
||||
t.Fatal("The paid amount should be set to 1000 even the challenge time window is out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGasUsed(t *testing.T) {
|
||||
tester := newTestContract(t)
|
||||
defer tester.teardown()
|
||||
|
||||
// Deposit 10,000 wei
|
||||
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
|
||||
customerOpt.Value = big.NewInt(10000)
|
||||
tx, _ := tester.contract.Deposit(customerOpt)
|
||||
tester.sim.Commit()
|
||||
|
||||
r, _ := tester.sim.TransactionReceipt(context.Background(), tx.Hash())
|
||||
fmt.Println("Deposit => gas used:", r.GasUsed)
|
||||
|
||||
// Second deposit
|
||||
customerOpt.Value = big.NewInt(10000)
|
||||
tx, _ = tester.contract.Deposit(customerOpt)
|
||||
tester.sim.Commit()
|
||||
|
||||
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
|
||||
fmt.Println("Deposit 2 => gas used:", r.GasUsed)
|
||||
|
||||
// Customer issues a cheque with amount 1000
|
||||
sig := tester.issueCheque(big.NewInt(1000))
|
||||
|
||||
// Customer tries to withdraw all money including the spent part
|
||||
customerOpt.Value = nil
|
||||
tx, _ = tester.contract.Withdraw(customerOpt, big.NewInt(9000))
|
||||
tester.sim.Commit()
|
||||
|
||||
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
|
||||
fmt.Println("Withdraw request => gas used:", r.GasUsed)
|
||||
|
||||
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
|
||||
|
||||
// Now all the money has been claimed.
|
||||
tx, _ = tester.contract.Claim(customerOpt)
|
||||
tester.sim.Commit()
|
||||
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
|
||||
fmt.Println("Deposit claim => gas used:", r.GasUsed)
|
||||
|
||||
//
|
||||
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
|
||||
tx, _ = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
|
||||
tester.sim.Commit()
|
||||
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
|
||||
fmt.Println("Cash cheque => gas used:", r.GasUsed)
|
||||
}
|
||||
230
contracts/accountbook/drawee.go
Normal file
230
contracts/accountbook/drawee.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accountbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/contracts/accountbook/contract"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ChequeDrawee represents the payment drawee in a off-chain payment channel.
|
||||
type ChequeDrawee struct {
|
||||
address common.Address
|
||||
cdb *chequeDB
|
||||
book *AccountBook
|
||||
opts *bind.TransactOpts
|
||||
cBackend bind.ContractBackend
|
||||
dBackend bind.DeployBackend
|
||||
}
|
||||
|
||||
// NewChequeDrawee creates a payment drawee and deploys the contract if necessary.
|
||||
func NewChequeDrawee(selfAddr common.Address, opts *bind.TransactOpts, contractBackend bind.ContractBackend, deployBackend bind.DeployBackend, db ethdb.Database) (*ChequeDrawee, error) {
|
||||
cdb := newChequeDB(db)
|
||||
var chanAddr common.Address
|
||||
stored := cdb.readContractAddr(selfAddr)
|
||||
if stored == nil {
|
||||
addr, err := deployAccountBook(opts, contractBackend, deployBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cdb.writeContractAddr(selfAddr, addr)
|
||||
chanAddr = addr
|
||||
} else {
|
||||
chanAddr = *stored
|
||||
}
|
||||
book, err := newAccountBook(chanAddr, contractBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drawee := &ChequeDrawee{
|
||||
address: selfAddr,
|
||||
cdb: cdb,
|
||||
book: book,
|
||||
opts: opts,
|
||||
cBackend: contractBackend,
|
||||
dBackend: deployBackend,
|
||||
}
|
||||
return drawee, nil
|
||||
}
|
||||
|
||||
// ContractAddr returns the address of deployed accountbook contract.
|
||||
func (drawee *ChequeDrawee) ContractAddr() common.Address {
|
||||
return drawee.book.address
|
||||
}
|
||||
|
||||
// AddCheque receives a cheque from drawer, check the validity and store
|
||||
// it locally.
|
||||
//
|
||||
// In the mean time, this function will return the cumulative uncash amount
|
||||
// for auto cash triggering.
|
||||
func (drawee *ChequeDrawee) AddCheque(c *Cheque) (*big.Int, *big.Int, error) {
|
||||
// Ensure the cheque is signed properly
|
||||
if err := c.validate(drawee.ContractAddr()); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Ensure the drawer has enough balance to cover the expense.
|
||||
unspent, err := drawee.Unspent(c.Drawer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Figure out the net amount of this cheque.
|
||||
lastReceived := drawee.cdb.readCheque(drawee.book.address, c.Drawer)
|
||||
var net *big.Int
|
||||
if lastReceived == nil {
|
||||
net = c.Amount
|
||||
} else if lastReceived.Amount.Cmp(c.Amount) >= 0 {
|
||||
// There are many cases can lead to this situation:
|
||||
// * Drawer passes a stale cheque deliberately
|
||||
// * Drawer's chequedb is broken, it loses all payment history
|
||||
// In order to help drawer to recover the payment history,
|
||||
// return an evidence here.
|
||||
return nil, nil, &StaleChequeError{Msg: "stale cheque", Evidence: lastReceived}
|
||||
} else {
|
||||
net = new(big.Int).Sub(c.Amount, lastReceived.Amount)
|
||||
}
|
||||
if unspent.Cmp(net) < 0 {
|
||||
return nil, nil, ErrNotEnoughDeposit
|
||||
}
|
||||
// Calculate uncashed amount from this drawer.
|
||||
paid, err := drawee.book.contract.Paids(nil, c.Drawer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Pass the validation, save it into disk.
|
||||
drawee.cdb.writeCheque(drawee.book.address, c.Drawer, c)
|
||||
return net, new(big.Int).Sub(c.Amount, paid), nil
|
||||
}
|
||||
|
||||
// +------------- Deposit -----------+
|
||||
// | |
|
||||
// +-----------+-----------+------------+
|
||||
// | Spent | Unspent | Withdrawed |
|
||||
// +-----------+-----------+-----------+------------+
|
||||
// | Paid | Unpaid |
|
||||
// +-----------+-----------+
|
||||
// | |
|
||||
// +--- Total Issued ---+
|
||||
|
||||
// Unpaid returns unpaid amount of the specified drawer in the channel.
|
||||
// The calculation method is using total_received(total_issued) minus cashed(paid).
|
||||
// However the total_received record can be missing due to db corrupt. If this happen,
|
||||
// we can only adjust the total_received to paid amount and drawer can double-spend
|
||||
// the uncashed part.
|
||||
// If the returned error is nil, the returned value should always be non-nil.
|
||||
func (drawee *ChequeDrawee) Unpaid(addr common.Address) (*big.Int, error) {
|
||||
// Check how much we have already cashed
|
||||
paid, err := drawee.book.contract.Paids(nil, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastReceived := drawee.cdb.readCheque(drawee.book.address, addr)
|
||||
// We never receive the cheque from this address or local db is corrupt
|
||||
if lastReceived == nil {
|
||||
// We have cashing record in contract, but total_received is missing,
|
||||
// db is corrupt.
|
||||
if paid.Uint64() != 0 {
|
||||
// Write a cheque without signature and drawer, we only need the issued amount.
|
||||
drawee.cdb.writeCheque(drawee.book.address, addr, &Cheque{Amount: paid, ContractAddr: drawee.ContractAddr()})
|
||||
}
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
// We have cashing record in contract, but total_received is lower than
|
||||
// the cashed amount, db is corrupt.
|
||||
if lastReceived.Amount.Cmp(paid) < 0 {
|
||||
// Write a cheque without signature and drawer, we only need the issued amount.
|
||||
drawee.cdb.writeCheque(drawee.book.address, addr, &Cheque{Amount: paid, ContractAddr: drawee.ContractAddr()})
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
return new(big.Int).Sub(lastReceived.Amount, paid), nil
|
||||
}
|
||||
|
||||
// Unspent returns all unspent balance of the specified drawer in the channel.
|
||||
// According to the diagram of balance, we can see unspent part is: deposit-withdrawed-unpaid.
|
||||
// If the returned error is nil, the returned balance should always be non-nil.
|
||||
func (drawee *ChequeDrawee) Unspent(addr common.Address) (*big.Int, error) {
|
||||
// Fetch deposit balance from the contract.
|
||||
balance, err := drawee.book.contract.Deposits(nil, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unpaid, err := drawee.Unpaid(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If no spendable balance or even worse the drawer already spends the
|
||||
// money exceeds all deposit.
|
||||
if unpaid.Cmp(balance) > 0 {
|
||||
return nil, ErrNotEnoughDeposit
|
||||
}
|
||||
remaining := new(big.Int).Sub(balance, unpaid)
|
||||
req, err := drawee.book.contract.WithdrawRequests(nil, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Drawer has a opened withdrawal request, no matter it passes the challenge
|
||||
// period or not, minus this part.
|
||||
if remaining.Cmp(req.Amount) < 0 {
|
||||
return nil, ErrNotEnoughDeposit
|
||||
}
|
||||
return new(big.Int).Sub(remaining, req.Amount), nil
|
||||
}
|
||||
|
||||
// Cash cashes all unpaid payment by given drawer.
|
||||
func (drawee *ChequeDrawee) Cash(context context.Context, drawer common.Address, sync bool) error {
|
||||
unpaid, err := drawee.Unpaid(drawer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unpaid.Uint64() == 0 {
|
||||
return nil // Nothing to cash
|
||||
}
|
||||
lastReceived := drawee.cdb.readCheque(drawee.book.address, drawer) // Can't be nil here.
|
||||
tx, err := drawee.book.contract.Cash(drawee.opts, drawer, lastReceived.Amount, lastReceived.Sig[64], common.BytesToHash(lastReceived.Sig[:32]), common.BytesToHash(lastReceived.Sig[32:64]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sync {
|
||||
_, err := bind.WaitMined(context, drawee.dBackend, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info("Cashed cheque", "drawer", drawer, "amount", unpaid, "cumulative", lastReceived.Amount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListenWithdraw watches new withdraw event triggered by drawers.
|
||||
func (drawee *ChequeDrawee) ListenWithdraw() (event.Subscription, chan *contract.AccountBookWithdrawEvent, error) {
|
||||
sink := make(chan *contract.AccountBookWithdrawEvent)
|
||||
sub, err := drawee.book.contract.WatchWithdrawEvent(nil, sink, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sub, sink, nil
|
||||
}
|
||||
|
||||
// ListCheques returns all cheques drawee received.
|
||||
func (drawee *ChequeDrawee) ListCheques() []*Cheque {
|
||||
return drawee.cdb.allCheques(drawee.ContractAddr())
|
||||
}
|
||||
259
contracts/accountbook/drawee_test.go
Normal file
259
contracts/accountbook/drawee_test.go
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accountbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
)
|
||||
|
||||
type testEnv struct {
|
||||
db ethdb.Database
|
||||
draweeKey *ecdsa.PrivateKey
|
||||
draweeAddr common.Address
|
||||
drawerKey *ecdsa.PrivateKey
|
||||
drawerAddr common.Address
|
||||
backend *backends.SimulatedBackend
|
||||
}
|
||||
|
||||
func newTestEnv(t *testing.T) *testEnv {
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
key, _ := crypto.GenerateKey()
|
||||
key2, _ := crypto.GenerateKey()
|
||||
addr := crypto.PubkeyToAddress(key.PublicKey)
|
||||
addr2 := crypto.PubkeyToAddress(key2.PublicKey)
|
||||
sim := backends.NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(1000000000)}, addr2: {Balance: big.NewInt(1000000000)}}, 10000000)
|
||||
return &testEnv{
|
||||
db: db,
|
||||
draweeKey: key,
|
||||
draweeAddr: addr,
|
||||
drawerKey: key2,
|
||||
drawerAddr: addr2,
|
||||
backend: sim,
|
||||
}
|
||||
}
|
||||
|
||||
func (env *testEnv) close() { env.backend.Close() }
|
||||
func (env *testEnv) issueCheque(amount *big.Int, chanAddr common.Address) *Cheque {
|
||||
cheque := &Cheque{
|
||||
Drawer: env.drawerAddr,
|
||||
ContractAddr: chanAddr,
|
||||
Amount: amount,
|
||||
}
|
||||
cheque.signWithKey(func(digestHash []byte) ([]byte, error) {
|
||||
sig, _ := crypto.Sign(digestHash, env.drawerKey)
|
||||
return sig, nil
|
||||
})
|
||||
return cheque
|
||||
}
|
||||
|
||||
func (env *testEnv) spendAndCheck(t *testing.T, drawee *ChequeDrawee, amount *big.Int, expectErr error, expectNet *big.Int, expectUnpaid *big.Int, expectUnspent *big.Int) {
|
||||
cheque := env.issueCheque(amount, drawee.ContractAddr())
|
||||
net, unpaid, err := drawee.AddCheque(cheque)
|
||||
if expectErr != nil {
|
||||
if err.Error() != expectErr.Error() {
|
||||
t.Fatalf("Error mismatch, want: %v, got: %v", expectErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if net.Cmp(expectNet) != 0 {
|
||||
t.Fatalf("Net amount mismatch, want: %v, got: %v", expectNet, net)
|
||||
}
|
||||
if unpaid.Cmp(expectUnpaid) != 0 {
|
||||
t.Fatalf("Unpaid amount mismatch, want: %v, got: %v", expectUnpaid, unpaid)
|
||||
}
|
||||
unspent, _ := drawee.Unspent(env.drawerAddr)
|
||||
if unspent.Cmp(expectUnspent) != 0 {
|
||||
t.Fatalf("Unspent amount mismatch, want: %v, got: %v", expectUnspent, unspent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployment(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
defer env.close()
|
||||
|
||||
var exit = make(chan struct{})
|
||||
defer close(exit)
|
||||
|
||||
// Start the automatic blockchain.
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
env.backend.Commit()
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Deploy the contract if missing
|
||||
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
|
||||
if err != nil {
|
||||
t.Fatalf("Faield to deploy contract, err: %v", err)
|
||||
}
|
||||
addr := drawee.cdb.readContractAddr(env.draweeAddr)
|
||||
if addr == nil {
|
||||
t.Fatalf("Failed to deploy contract")
|
||||
}
|
||||
if *addr != drawee.book.address {
|
||||
t.Fatalf("Contract address mismatch, want: %v, got: %v", drawee.book.address, *addr)
|
||||
}
|
||||
// Restart, no deploy needed
|
||||
drawee, _ = NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
|
||||
addr2 := drawee.cdb.readContractAddr(env.draweeAddr)
|
||||
if addr2 == nil {
|
||||
t.Fatalf("Failed to reload contract")
|
||||
}
|
||||
if *addr2 != *addr {
|
||||
t.Fatalf("Contract address mismatch, want: %v, got: %v", *addr, *addr2)
|
||||
}
|
||||
// Remove the db explicitly
|
||||
newdb := rawdb.NewMemoryDatabase()
|
||||
drawee, _ = NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, newdb)
|
||||
addr3 := drawee.cdb.readContractAddr(env.draweeAddr)
|
||||
if addr3 == nil {
|
||||
t.Fatalf("Failed to re-deploy contract")
|
||||
}
|
||||
if *addr3 == *addr {
|
||||
t.Fatalf("New contract address expected")
|
||||
}
|
||||
// Drawee changes the key, new contract should be deployed
|
||||
drawee, _ = NewChequeDrawee(env.drawerAddr, bind.NewKeyedTransactor(env.drawerKey), env.backend, env.backend, newdb)
|
||||
addr4 := drawee.cdb.readContractAddr(env.drawerAddr)
|
||||
if addr4 == nil {
|
||||
t.Fatalf("Failed to re-deploy contract")
|
||||
}
|
||||
if *addr4 == *addr3 {
|
||||
t.Fatalf("New contract address expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddCheque(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
defer env.close()
|
||||
|
||||
var exit = make(chan struct{})
|
||||
defer close(exit)
|
||||
|
||||
// Start the automatic blockchain.
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
env.backend.Commit()
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Deploy the contract if missing
|
||||
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
|
||||
if err != nil {
|
||||
t.Fatalf("Faield to deploy contract, err: %v", err)
|
||||
}
|
||||
// Ensure we can reject all cheques which doesn't enough fund backup
|
||||
env.spendAndCheck(t, drawee, big.NewInt(100), ErrNotEnoughDeposit, nil, nil, nil)
|
||||
|
||||
// Deposit enough money for drawer
|
||||
opt := bind.NewKeyedTransactor(env.drawerKey)
|
||||
opt.Value = big.NewInt(200)
|
||||
tx, _ := drawee.book.contract.Deposit(opt)
|
||||
bind.WaitMined(context.Background(), env.backend, tx)
|
||||
unspent, _ := drawee.Unspent(env.drawerAddr)
|
||||
if unspent.Uint64() != 200 {
|
||||
t.Fatalf("Balance mismatch")
|
||||
}
|
||||
// Spend 100, ensure it's successful
|
||||
env.spendAndCheck(t, drawee, big.NewInt(100), nil, big.NewInt(100), big.NewInt(100), big.NewInt(100))
|
||||
|
||||
// Spend another 100, ensure it's also successful
|
||||
env.spendAndCheck(t, drawee, big.NewInt(200), nil, big.NewInt(100), big.NewInt(200), big.NewInt(0))
|
||||
|
||||
// Remove the cheque db explicitly
|
||||
drawee.cdb = newChequeDB(rawdb.NewMemoryDatabase())
|
||||
|
||||
// Drawer can double-spend the part which we haven't cashed.
|
||||
env.spendAndCheck(t, drawee, big.NewInt(100), nil, big.NewInt(100), big.NewInt(100), big.NewInt(100))
|
||||
env.spendAndCheck(t, drawee, big.NewInt(150), nil, big.NewInt(50), big.NewInt(150), big.NewInt(50))
|
||||
|
||||
// Cash all received payments
|
||||
drawee.Cash(context.Background(), env.drawerAddr, true)
|
||||
// Remove the cheque db explicitly again
|
||||
drawee.cdb = newChequeDB(rawdb.NewMemoryDatabase())
|
||||
|
||||
// We can repair the broken db and reject stale cheque
|
||||
env.spendAndCheck(t, drawee, big.NewInt(100), &StaleChequeError{Msg: "stale cheque"}, nil, nil, nil)
|
||||
env.spendAndCheck(t, drawee, big.NewInt(150), &StaleChequeError{Msg: "stale cheque"}, nil, nil, nil)
|
||||
env.spendAndCheck(t, drawee, big.NewInt(200), nil, big.NewInt(50), big.NewInt(50), big.NewInt(0))
|
||||
env.spendAndCheck(t, drawee, big.NewInt(250), ErrNotEnoughDeposit, nil, nil, nil)
|
||||
}
|
||||
|
||||
// This function tests a special scenario:
|
||||
// drawer deposits some money and then opens the withdrawal request quickly.
|
||||
// Then it wants to spend some withdrawed money.
|
||||
func TestWithdrawInAdvance(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
defer env.close()
|
||||
|
||||
var exit = make(chan struct{})
|
||||
defer close(exit)
|
||||
|
||||
// Start the automatic blockchain.
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
env.backend.Commit()
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Deploy the contract if missing
|
||||
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
|
||||
if err != nil {
|
||||
t.Fatalf("Faield to deploy contract, err: %v", err)
|
||||
}
|
||||
// Deposit enough money for drawer
|
||||
opt := bind.NewKeyedTransactor(env.drawerKey)
|
||||
opt.Value = big.NewInt(200)
|
||||
tx, _ := drawee.book.contract.Deposit(opt)
|
||||
bind.WaitMined(context.Background(), env.backend, tx)
|
||||
|
||||
// Open withdrawal request immediately
|
||||
opt = bind.NewKeyedTransactor(env.drawerKey)
|
||||
tx, _ = drawee.book.contract.Withdraw(opt, big.NewInt(150))
|
||||
bind.WaitMined(context.Background(), env.backend, tx)
|
||||
|
||||
// Ensure all withdrawed part can't be double spend
|
||||
env.spendAndCheck(t, drawee, big.NewInt(100), ErrNotEnoughDeposit, nil, nil, nil)
|
||||
env.spendAndCheck(t, drawee, big.NewInt(50), nil, big.NewInt(50), big.NewInt(50), big.NewInt(0))
|
||||
}
|
||||
314
contracts/accountbook/drawer.go
Normal file
314
contracts/accountbook/drawer.go
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accountbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ChequeDrawer represents the payment drawer in a off-chain payment channel.
|
||||
type ChequeDrawer struct {
|
||||
withdrawRequest uint64
|
||||
cdb *chequeDB
|
||||
contractBackend bind.ContractBackend
|
||||
deployBackend bind.DeployBackend
|
||||
book *AccountBook
|
||||
selfAddr common.Address
|
||||
txSigner *bind.TransactOpts
|
||||
|
||||
keySigner func(data []byte) ([]byte, error) // Used for testing, cheque signer
|
||||
chequeSigner func(data []byte) ([]byte, error) // Used for production environment, cheque signer
|
||||
}
|
||||
|
||||
func NewChequeDrawer(txSigner *bind.TransactOpts, chequeSigner func(data []byte) ([]byte, error), selfAddr common.Address, contractAddr common.Address, contractBackend bind.ContractBackend, deployBackend bind.DeployBackend, db ethdb.Database) (*ChequeDrawer, error) {
|
||||
if contractAddr == (common.Address{}) {
|
||||
return nil, errors.New("empty contract address")
|
||||
}
|
||||
book, err := newAccountBook(contractAddr, contractBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drawer := &ChequeDrawer{
|
||||
cdb: newChequeDB(db),
|
||||
contractBackend: contractBackend,
|
||||
deployBackend: deployBackend,
|
||||
selfAddr: selfAddr,
|
||||
book: book,
|
||||
txSigner: txSigner,
|
||||
chequeSigner: chequeSigner,
|
||||
}
|
||||
return drawer, nil
|
||||
}
|
||||
|
||||
// ContractAddr returns the address of deployed accountbook contract.
|
||||
func (drawer *ChequeDrawer) ContractAddr() common.Address {
|
||||
return drawer.book.address
|
||||
}
|
||||
|
||||
// Deposit transfers the given amount wei into the contract. If the sync is true,
|
||||
// this function will wait until the transaction is included and return the actual
|
||||
// transaction status.
|
||||
func (drawer *ChequeDrawer) Deposit(context context.Context, amount *big.Int) (bool, error) {
|
||||
// Create an independent auth opt
|
||||
depositOpt := &bind.TransactOpts{
|
||||
From: drawer.selfAddr,
|
||||
Signer: drawer.txSigner.Signer,
|
||||
Value: amount,
|
||||
}
|
||||
tx, err := drawer.book.contract.Deposit(depositOpt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
receipt, err := bind.WaitMined(context, drawer.deployBackend, tx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return receipt.Status == types.ReceiptStatusSuccessful, nil
|
||||
}
|
||||
|
||||
// +------------- Deposit ------------+
|
||||
// | |
|
||||
// +-----------+-----------+------------+
|
||||
// | Spent | Unspent | Withdrawed |
|
||||
// +-----------+-----------+-----------+------------+
|
||||
// | Paid | Unpaid |
|
||||
// +-----------+-----------+
|
||||
// | |
|
||||
// +--- Total Issued ---+
|
||||
|
||||
// Unpaid returns unpaid amount in the channel. The calculation method is using
|
||||
// total_issued minus paid. However the total_issued record can be missing due
|
||||
// to db corrupt. If this happen, we can reset the total issued amount by repair.
|
||||
// If the returned error is nil, the returned value should always be non-nil.
|
||||
//
|
||||
// todo(rjl493456442) it's too expensive for light client to retrieve paid amount
|
||||
// every time.
|
||||
func (drawer *ChequeDrawer) Unpaid() (*big.Int, error) {
|
||||
// Check how much we have already cashed
|
||||
paid, err := drawer.book.contract.Paids(nil, drawer.selfAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastIssued := drawer.cdb.readLastIssued(drawer.selfAddr, drawer.book.address)
|
||||
// We never issue any cheque from this address or local db is corrupt
|
||||
if lastIssued == nil {
|
||||
// Drawee has already cashed a few cheques, local db must be corrupt,
|
||||
// repair it. But we still have no clue how much we have issued which
|
||||
// is not cashed by drawee yet.
|
||||
if paid.Uint64() != 0 {
|
||||
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, paid)
|
||||
}
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
// Drawee has already cashed a few cheques, but total_issued is lower than
|
||||
// the cashed amount, db is corrupt. But we still have no clue how much we
|
||||
// have issued which is not cashed by drawee yet.
|
||||
if lastIssued.Cmp(paid) < 0 {
|
||||
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, paid)
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
return new(big.Int).Sub(lastIssued, paid), nil
|
||||
}
|
||||
|
||||
// Unspent returns all unspent balance of ourselves in the channel. It can
|
||||
// happen that we get a larger value then real unspent amount due to the
|
||||
// data loss.
|
||||
// If the returned error is nil, the returned balance should always be non-nil.
|
||||
func (drawer *ChequeDrawer) Unspent() (*big.Int, error) {
|
||||
// Fetch deposit balance from the contract.
|
||||
balance, err := drawer.book.contract.Deposits(nil, drawer.selfAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unpaid, err := drawer.Unpaid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If no spendable balance or even worse we already spend the
|
||||
// money exceeds all deposit.
|
||||
if unpaid.Cmp(balance) > 0 {
|
||||
return big.NewInt(0), ErrNotEnoughDeposit
|
||||
}
|
||||
remaining := new(big.Int).Sub(balance, unpaid)
|
||||
req, err := drawer.book.contract.WithdrawRequests(nil, drawer.selfAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Drawer has a opened withdrawal request, no matter it passes the challenge
|
||||
// period or not, minus this part.
|
||||
if req.Amount.Uint64() != 0 && remaining.Cmp(req.Amount) < 0 {
|
||||
return big.NewInt(0), ErrNotEnoughDeposit
|
||||
}
|
||||
return new(big.Int).Sub(remaining, req.Amount), nil
|
||||
}
|
||||
|
||||
// IssueCheque creates a cheque for issuing specified amount money for payee.
|
||||
//
|
||||
// Whenever the drawer creates a cheque and sends it to drawee, drawee has
|
||||
// the permission to cash the deposit of drawer in the contract.
|
||||
//
|
||||
// Because of the possible data loss, we can issue some double-spend cheques,
|
||||
// they will be rejected by drawee.
|
||||
//
|
||||
// In the mean time, this function will also return the remaining unspent
|
||||
// to trigger auto deposit.
|
||||
func (drawer *ChequeDrawer) IssueCheque(amount *big.Int) (*Cheque, *big.Int, error) {
|
||||
if amount == nil || amount.Uint64() == 0 {
|
||||
return nil, nil, errors.New("invalid issue amount")
|
||||
}
|
||||
unspent, err := drawer.Unspent()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if unspent.Cmp(amount) < 0 {
|
||||
return nil, nil, ErrNotEnoughDeposit
|
||||
}
|
||||
var newAmount *big.Int
|
||||
// If local chequedb is broken, the new amount maybe is a invalid stale number.
|
||||
// Finally drawee will show us the evidence which we signed before, we can repair
|
||||
// broken db.
|
||||
lastIssued := drawer.cdb.readLastIssued(drawer.selfAddr, drawer.book.address)
|
||||
if lastIssued == nil {
|
||||
newAmount = amount
|
||||
} else {
|
||||
newAmount = new(big.Int).Add(lastIssued, amount)
|
||||
}
|
||||
// Assmeble the cheque and sign it.
|
||||
cheque := &Cheque{
|
||||
Drawer: drawer.selfAddr,
|
||||
Amount: newAmount,
|
||||
ContractAddr: drawer.book.address,
|
||||
}
|
||||
// Uses keySigner if we are testing.
|
||||
if drawer.keySigner != nil {
|
||||
if err := cheque.signWithKey(drawer.keySigner); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
if err := cheque.sign(drawer.chequeSigner); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, newAmount)
|
||||
return cheque, new(big.Int).Sub(unspent, amount), nil
|
||||
}
|
||||
|
||||
// Withdraw submits a on-chain transaction to open the withdrawal request to
|
||||
// withdraw all deposit.
|
||||
func (drawer *ChequeDrawer) Withdraw(context context.Context) error {
|
||||
// In the channel contract, we only allow one withdrawal operation
|
||||
// at the same time. Ensure there is no opened withdrawl request.
|
||||
req, err := drawer.book.contract.WithdrawRequests(nil, drawer.selfAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Amount.Uint64() != 0 {
|
||||
atomic.StoreUint64(&drawer.withdrawRequest, req.CreatedAt.Uint64())
|
||||
log.Info("Ongoing withdrawal operation", "amount", req.Amount, "createAt", req.CreatedAt)
|
||||
return errors.New("duplicate withdraw operation")
|
||||
}
|
||||
unspent, err := drawer.Unspent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unspent.Uint64() == 0 {
|
||||
return errors.New("no withdrawable balance")
|
||||
}
|
||||
// todo(rjl493456442) add a threshold checking, ignore small balance.
|
||||
tx, err := drawer.book.contract.Withdraw(drawer.txSigner, unspent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
receipt, err := bind.WaitMined(context, drawer.deployBackend, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if receipt.Status == types.ReceiptStatusSuccessful {
|
||||
ret, err := drawer.book.contract.WithdrawRequests(nil, drawer.selfAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
atomic.StoreUint64(&drawer.withdrawRequest, ret.CreatedAt.Uint64())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithdrawalRecord returns the create block number of ongoing withdrawal request.
|
||||
func (drawer *ChequeDrawer) WithdrawalRecord() uint64 {
|
||||
return atomic.LoadUint64(&drawer.withdrawRequest)
|
||||
}
|
||||
|
||||
// ResetWithdrawlRecord resets withdrawal record when we submit the cash transaction.
|
||||
func (drawer *ChequeDrawer) ResetWithdrawlRecord() {
|
||||
atomic.StoreUint64(&drawer.withdrawRequest, 0)
|
||||
}
|
||||
|
||||
// Claim submits a on-chain transaction to claim all claimable balance.
|
||||
func (drawer *ChequeDrawer) Claim(context context.Context) (bool, error) {
|
||||
tx, err := drawer.book.contract.Claim(drawer.txSigner)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
receipt, err := bind.WaitMined(context, drawer.deployBackend, tx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return receipt.Status == types.ReceiptStatusSuccessful, nil
|
||||
}
|
||||
|
||||
// CodeHash returns the code hash of payment channel.
|
||||
func (drawer *ChequeDrawer) CodeHash(context context.Context) (common.Hash, error) {
|
||||
code, err := drawer.deployBackend.CodeAt(context, drawer.book.address, nil)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return crypto.Keccak256Hash(code), nil
|
||||
}
|
||||
|
||||
// Amend amends the local cheque db with externally provided cheque which is issued
|
||||
// by ourselves.
|
||||
func (drawer *ChequeDrawer) Amend(cheque *Cheque) error {
|
||||
if err := cheque.validate(drawer.book.address); err != nil {
|
||||
return err
|
||||
}
|
||||
if cheque.Drawer != drawer.selfAddr {
|
||||
return errors.New("invalid evidence")
|
||||
}
|
||||
// If local chequedb is corrupt, we can lose some payment records.
|
||||
// Since the amount of cheque is cumulative, so we need the evidence
|
||||
// from drawee to amend the local db.
|
||||
lastIssued := drawer.cdb.readLastIssued(drawer.selfAddr, drawer.book.address)
|
||||
if lastIssued == nil || lastIssued.Cmp(cheque.Amount) < 0 {
|
||||
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, cheque.Amount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Payed returns the total payed amount in this channel.
|
||||
func (drawer *ChequeDrawer) Payed() *big.Int {
|
||||
return drawer.cdb.readLastIssued(drawer.selfAddr, drawer.ContractAddr())
|
||||
}
|
||||
174
contracts/accountbook/drawer_test.go
Normal file
174
contracts/accountbook/drawer_test.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accountbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func (env *testEnv) commitEmptyBlocks(n int) {
|
||||
for i := 0; i < n; i++ {
|
||||
env.backend.Commit()
|
||||
}
|
||||
}
|
||||
func (env *testEnv) issueAndCheck(t *testing.T, drawer *ChequeDrawer, amount *big.Int, expectErr error, expectCum *big.Int, expectUnspent *big.Int) *Cheque {
|
||||
cheque, unspent, err := drawer.IssueCheque(amount)
|
||||
if expectErr != nil {
|
||||
if err.Error() != expectErr.Error() {
|
||||
t.Fatalf("Error mismatch, want: %v, got: %v", expectErr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if unspent.Cmp(expectUnspent) != 0 {
|
||||
t.Fatalf("Unspent amount mismatch, want: %v, got: %v", expectUnspent, unspent)
|
||||
}
|
||||
if cheque.Amount.Cmp(expectCum) != 0 {
|
||||
t.Fatalf("Cumulative spent amount mismatch, want: %v, got: %v", expectCum, cheque.Amount)
|
||||
}
|
||||
return cheque
|
||||
}
|
||||
|
||||
func TestIssueCheque(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
defer env.close()
|
||||
|
||||
var exit = make(chan struct{})
|
||||
defer close(exit)
|
||||
|
||||
// Start the automatic blockchain.
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
env.backend.Commit()
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Deploy the contract if missing
|
||||
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
|
||||
if err != nil {
|
||||
t.Fatalf("Faield to deploy contract, err: %v", err)
|
||||
}
|
||||
drawer, err := NewChequeDrawer(bind.NewKeyedTransactor(env.drawerKey), nil, env.drawerAddr, drawee.ContractAddr(), env.backend, env.backend, env.db)
|
||||
if err != nil {
|
||||
t.Fatalf("Faield to create drawer, err: %v", err)
|
||||
}
|
||||
drawer.keySigner = func(data []byte) ([]byte, error) {
|
||||
sig, _ := crypto.Sign(data, env.drawerKey)
|
||||
return sig, nil
|
||||
}
|
||||
// Reject all invalid issue operation
|
||||
env.issueAndCheck(t, drawer, big.NewInt(0), errors.New("invalid issue amount"), nil, nil)
|
||||
env.issueAndCheck(t, drawer, big.NewInt(100), ErrNotEnoughDeposit, nil, nil)
|
||||
|
||||
// Deposit some funds into the contract
|
||||
drawer.Deposit(context.Background(), big.NewInt(200))
|
||||
env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(50), big.NewInt(150))
|
||||
lastIssued := env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(100), big.NewInt(100))
|
||||
|
||||
// Cash all payments
|
||||
tx, err := drawer.book.contract.Cash(bind.NewKeyedTransactor(env.draweeKey), env.drawerAddr, lastIssued.Amount, lastIssued.Sig[64], common.BytesToHash(lastIssued.Sig[:32]), common.BytesToHash(lastIssued.Sig[32:64]))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to cash payment, err: %v", err)
|
||||
}
|
||||
bind.WaitMined(context.Background(), env.backend, tx)
|
||||
|
||||
// Remove chequedb explictly
|
||||
drawer.cdb = newChequeDB(rawdb.NewMemoryDatabase())
|
||||
// Ensure we can repair the broken db!
|
||||
env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(150), big.NewInt(50))
|
||||
lastIssued = env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(200), big.NewInt(0))
|
||||
env.issueAndCheck(t, drawer, big.NewInt(50), ErrNotEnoughDeposit, nil, nil)
|
||||
|
||||
// Remove chequedb explictly again!
|
||||
drawer.cdb = newChequeDB(rawdb.NewMemoryDatabase())
|
||||
// Amend the broken chequedb
|
||||
if err := drawer.Amend(lastIssued); err != nil {
|
||||
t.Fatalf("Failed to amend broken chequedb, err: %v", err)
|
||||
}
|
||||
unspent, err := drawer.Unspent()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve unspent part, err: %v", err)
|
||||
}
|
||||
if unspent.Uint64() != 0 {
|
||||
t.Fatalf("Failed to ament the chequedb")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdrawAndClaim(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
defer env.close()
|
||||
|
||||
var exit = make(chan struct{})
|
||||
defer close(exit)
|
||||
|
||||
// Start the automatic blockchain.
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
env.backend.Commit()
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Deploy the contract if missing
|
||||
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
|
||||
if err != nil {
|
||||
t.Fatalf("Faield to deploy contract, err: %v", err)
|
||||
}
|
||||
drawer, err := NewChequeDrawer(bind.NewKeyedTransactor(env.drawerKey), nil, env.drawerAddr, drawee.ContractAddr(), env.backend, env.backend, env.db)
|
||||
if err != nil {
|
||||
t.Fatalf("Faield to create drawer, err: %v", err)
|
||||
}
|
||||
drawer.keySigner = func(data []byte) ([]byte, error) {
|
||||
sig, _ := crypto.Sign(data, env.drawerKey)
|
||||
return sig, nil
|
||||
}
|
||||
// Try to withdraw money, but we have no deposit.
|
||||
if err := drawer.Withdraw(context.Background()); err == nil {
|
||||
t.Fatal("Failed to reject invalid withdrawal request")
|
||||
}
|
||||
drawer.Deposit(context.Background(), big.NewInt(200))
|
||||
|
||||
if err := drawer.Withdraw(context.Background()); err != nil {
|
||||
t.Fatalf("Failed to open withdrawal request, err: %v", err)
|
||||
}
|
||||
if err := drawer.Withdraw(context.Background()); err == nil {
|
||||
t.Fatal("Duplicated withdrawal request should be rejected")
|
||||
}
|
||||
number := drawer.WithdrawalRecord()
|
||||
if number == 0 {
|
||||
t.Fatal("Withdrawal record should be set")
|
||||
}
|
||||
// After opening the withdrawal request, we can spend this part
|
||||
env.issueAndCheck(t, drawer, big.NewInt(100), ErrNotEnoughDeposit, nil, nil)
|
||||
}
|
||||
38
contracts/accountbook/errors.go
Normal file
38
contracts/accountbook/errors.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accountbook
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrNotEnoughDeposit is returned if the cheque drawer doesn't have enough
|
||||
// balance to spend. Whenever drawee receives this error, it should emit a
|
||||
// cash operation as soon as possible.
|
||||
var ErrNotEnoughDeposit = errors.New("deposit is not enough")
|
||||
|
||||
// StaleChequeError wraps a error msg and the evidence for a stale cheque.
|
||||
//
|
||||
// Cheque drawer can sign the stale cheques deliberately or indeliberately.
|
||||
// E.G. If the cheque db of drawer is missing, it can lead to a indeliberate
|
||||
// stale cheque.
|
||||
type StaleChequeError struct {
|
||||
Msg string
|
||||
Evidence *Cheque // The latest received cheque can be used as an evidence
|
||||
}
|
||||
|
||||
func (err *StaleChequeError) Error() string {
|
||||
return err.Msg
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ type LesServer interface {
|
|||
APIs() []rpc.API
|
||||
Protocols() []p2p.Protocol
|
||||
SetBloomBitsIndexer(bbIndexer *core.ChainIndexer)
|
||||
SetContractBackend(bind.ContractBackend)
|
||||
SetBackends(bind.ContractBackend, bind.DeployBackend)
|
||||
}
|
||||
|
||||
// Ethereum implements the Ethereum full node service.
|
||||
|
|
@ -103,10 +103,10 @@ func (s *Ethereum) AddLesServer(ls LesServer) {
|
|||
}
|
||||
|
||||
// SetClient sets a rpc client which connecting to our local node.
|
||||
func (s *Ethereum) SetContractBackend(backend bind.ContractBackend) {
|
||||
func (s *Ethereum) SetBackends(contract bind.ContractBackend, deploy bind.DeployBackend) {
|
||||
// Pass the rpc client to les server if it is enabled.
|
||||
if s.lesServer != nil {
|
||||
s.lesServer.SetContractBackend(backend)
|
||||
s.lesServer.SetBackends(contract, deploy)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,11 @@ type Config struct {
|
|||
UltraLightFraction int `toml:",omitempty"` // Percentage of trusted servers to accept an announcement
|
||||
UltraLightOnlyAnnounce bool `toml:",omitempty"` // Whether to only announce headers, or also serve them
|
||||
|
||||
// Les server incentivization options
|
||||
LightServiceCharge bool // Indicator whether to charge for the light service
|
||||
LightServicePay bool // Indicator whether to pay for the light service
|
||||
LightAddress common.Address // Address of the server or client which used to pay the fee or charge.
|
||||
|
||||
// Database options
|
||||
SkipBcVersionCheck bool `toml:"-"`
|
||||
DatabaseHandles int `toml:"-"`
|
||||
|
|
|
|||
|
|
@ -833,6 +833,11 @@ web3._extend({
|
|||
call: 'les_addBalance',
|
||||
params: 3
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'channelInfo',
|
||||
call: 'les_channelInfo',
|
||||
params: 1
|
||||
}),
|
||||
],
|
||||
properties:
|
||||
[
|
||||
|
|
@ -848,6 +853,10 @@ web3._extend({
|
|||
name: 'serverInfo',
|
||||
getter: 'les_serverInfo'
|
||||
}),
|
||||
new web3._extend.Property({
|
||||
name: 'openedChannels',
|
||||
getter: 'les_openedChannels'
|
||||
}),
|
||||
]
|
||||
});
|
||||
`
|
||||
|
|
|
|||
20
les/api.go
20
les/api.go
|
|
@ -20,10 +20,13 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/les/payment"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
)
|
||||
|
||||
|
|
@ -33,6 +36,7 @@ var (
|
|||
errUnknownBenchmarkType = errors.New("unknown benchmark type")
|
||||
errBalanceOverflow = errors.New("balance overflow")
|
||||
errNoPriority = errors.New("priority too low to raise capacity")
|
||||
errNoPayment = errors.New("payment channel hasn't been initialized")
|
||||
)
|
||||
|
||||
const maxBalance = math.MaxInt64
|
||||
|
|
@ -352,3 +356,19 @@ func (api *PrivateLightAPI) GetCheckpointContractAddress() (string, error) {
|
|||
}
|
||||
return api.backend.oracle.config.Address.Hex(), nil
|
||||
}
|
||||
|
||||
// OpenedChannel returns all established channel addresses.
|
||||
func (api *PrivateLightAPI) OpenedChannels() ([]common.Address, error) {
|
||||
if atomic.LoadUint32(&api.backend.paymentInited) == 0 {
|
||||
return nil, errNoPayment
|
||||
}
|
||||
return api.backend.channelManager.ChannelAddresses(), nil
|
||||
}
|
||||
|
||||
// ChannelInfo returns the info of the specified one.
|
||||
func (api *PrivateLightAPI) ChannelInfo(addr common.Address) (payment.ChannelInfo, error) {
|
||||
if atomic.LoadUint32(&api.backend.paymentInited) == 0 {
|
||||
return payment.ChannelInfo{}, errNoPayment
|
||||
}
|
||||
return api.backend.channelManager.ChannelInfo(addr), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ func (b *LesApiBackend) EventMux() *event.TypeMux {
|
|||
}
|
||||
|
||||
func (b *LesApiBackend) AccountManager() *accounts.Manager {
|
||||
return b.eth.accountManager
|
||||
return b.eth.am
|
||||
}
|
||||
|
||||
func (b *LesApiBackend) ExtRPCEnabled() bool {
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ func newLesServerService(ctx *adapters.ServiceContext) (node.Service, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server, err := NewLesServer(ethereum, &config)
|
||||
server, err := NewLesServer(nil, ethereum, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ func (h *serverHandler) measure(setup *benchmarkSetup, count int) error {
|
|||
}()
|
||||
go func() {
|
||||
for i := 0; i < count; i++ {
|
||||
if err := h.handleMsg(serverPeer, &sync.WaitGroup{}); err != nil {
|
||||
if err := h.handleMsg(serverPeer, &sync.WaitGroup{}, nil); err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package les
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
|
|
@ -36,6 +37,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/eth/gasprice"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||
"github.com/ethereum/go-ethereum/les/payment"
|
||||
"github.com/ethereum/go-ethereum/light"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
|
|
@ -63,7 +65,6 @@ type LightEthereum struct {
|
|||
ApiBackend *LesApiBackend
|
||||
eventMux *event.TypeMux
|
||||
engine consensus.Engine
|
||||
accountManager *accounts.Manager
|
||||
netRPCService *ethapi.PublicNetAPI
|
||||
}
|
||||
|
||||
|
|
@ -88,10 +89,10 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
|
|||
chainDb: chainDb,
|
||||
peers: peers,
|
||||
closeCh: make(chan struct{}),
|
||||
am: ctx.AccountManager,
|
||||
},
|
||||
eventMux: ctx.EventMux,
|
||||
reqDist: newRequestDistributor(peers, &mclock.System{}),
|
||||
accountManager: ctx.AccountManager,
|
||||
engine: eth.CreateConsensusEngine(ctx, chainConfig, &config.Ethash, nil, false, chainDb),
|
||||
bloomRequests: make(chan chan *bloombits.Retrieval),
|
||||
bloomIndexer: eth.NewBloomIndexer(chainDb, params.BloomBitsBlocksClient, params.HelperTrieConfirmations),
|
||||
|
|
@ -148,6 +149,15 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
|
|||
}
|
||||
leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams)
|
||||
|
||||
if config.LightServicePay {
|
||||
paymentDb, err := ctx.OpenDatabase("paymentdata", 0, 0, "eth/db/paymentdata") // How to disable metrics?
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
leth.paymentDb = paymentDb
|
||||
leth.address = config.LightAddress
|
||||
log.Warn("Payment db opened, please never delete it", "path", ctx.ResolvePath("eth/db/paymentdata"))
|
||||
}
|
||||
return leth, nil
|
||||
}
|
||||
|
||||
|
|
@ -264,15 +274,42 @@ func (s *LightEthereum) Stop() error {
|
|||
s.eventMux.Stop()
|
||||
s.serverPool.stop()
|
||||
s.chainDb.Close()
|
||||
if s.paymentDb != nil {
|
||||
s.paymentDb.Close()
|
||||
}
|
||||
s.wg.Wait()
|
||||
log.Info("Light ethereum stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetClient sets the rpc client and binds the registrar contract.
|
||||
func (s *LightEthereum) SetContractBackend(backend bind.ContractBackend) {
|
||||
if s.oracle == nil {
|
||||
func (s *LightEthereum) SetBackends(contract bind.ContractBackend, deploy bind.DeployBackend) {
|
||||
if s.oracle != nil {
|
||||
s.oracle.start(contract)
|
||||
}
|
||||
if s.config.LightServicePay {
|
||||
go func() {
|
||||
if s.address == (common.Address{}) {
|
||||
log.Warn("Failed to setup cheque drawee", "error", "empty cheque drawee address")
|
||||
return
|
||||
}
|
||||
s.oracle.start(backend)
|
||||
account := accounts.Account{Address: s.address}
|
||||
wallet, err := s.am.Find(account)
|
||||
if err != nil {
|
||||
log.Warn("Failed to setup cheque drawee", "error", err)
|
||||
return
|
||||
}
|
||||
chequeSigner := func(data []byte) ([]byte, error) {
|
||||
return wallet.SignData(account, accounts.MimetypeDataWithValidator, data)
|
||||
}
|
||||
channelManager, err := payment.NewPaymentChannelManager(payment.DefaultPaymentChannelDrawerConfig, s.chainReader, bind.NewRawTransactor(wallet.SignTx, account), chequeSigner, s.address, contract, deploy, s.paymentDb)
|
||||
if err != nil {
|
||||
log.Warn("Failed to setup cheque drawee", "error", err)
|
||||
return
|
||||
}
|
||||
s.channelManager = channelManager
|
||||
atomic.StoreUint32(&s.paymentInited, 1) // Mark payment channel is available now
|
||||
log.Info("Succeed to setup cheque drawee", "address", s.address)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,17 @@
|
|||
package les
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/les/payment"
|
||||
"github.com/ethereum/go-ethereum/light"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
|
|
@ -100,6 +103,10 @@ func (h *clientHandler) handle(p *peer) error {
|
|||
}
|
||||
p.Log().Debug("Light Ethereum peer connected", "name", p.Name())
|
||||
|
||||
// Ensure the payment is initialized before we establish any connections
|
||||
if h.backend.config.LightServicePay && atomic.LoadUint32(&h.backend.paymentInited) == 0 {
|
||||
return errors.New("payment hasn't been initialized")
|
||||
}
|
||||
// Execute the LES handshake
|
||||
var (
|
||||
head = h.backend.blockchain.CurrentHeader()
|
||||
|
|
@ -107,7 +114,7 @@ func (h *clientHandler) handle(p *peer) error {
|
|||
number = head.Number.Uint64()
|
||||
td = h.backend.blockchain.GetTd(hash, number)
|
||||
)
|
||||
if err := p.Handshake(td, hash, number, h.backend.blockchain.Genesis().Hash(), nil); err != nil {
|
||||
if err := p.Handshake(td, hash, number, h.backend.blockchain.Genesis().Hash(), nil, h.backend.address); err != nil {
|
||||
p.Log().Debug("Light Ethereum handshake failed", "err", err)
|
||||
return err
|
||||
}
|
||||
|
|
@ -131,9 +138,40 @@ func (h *clientHandler) handle(p *peer) error {
|
|||
if p.poolEntry != nil {
|
||||
h.backend.serverPool.registered(p.poolEntry)
|
||||
}
|
||||
// Open channel if server requests charging.
|
||||
var err error
|
||||
var payment payment.Payment
|
||||
if p.paymentChannel != (common.Address{}) {
|
||||
payment, err = h.backend.channelManager.OpenChannel(p.paymentChannel, p)
|
||||
if err != nil {
|
||||
p.Log().Error("Failed to open channel", "error", err)
|
||||
return err
|
||||
}
|
||||
p.Log().Info("Opened the channel", "address", p.paymentChannel, "id", p.id)
|
||||
defer h.backend.channelManager.CloseChannel(p.paymentChannel)
|
||||
|
||||
// todo code hash challenge, ensure the channel is trusted
|
||||
|
||||
// Toy payment loop, just for debugging
|
||||
exit := make(chan struct{})
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * 30)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := payment.Pay(new(big.Int).Mul(big.NewInt(1e6), big.NewInt(params.GWei))); err != nil {
|
||||
p.Log().Error("Failed to pay", "error", err)
|
||||
} // 1e6 gWei
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer close(exit)
|
||||
}
|
||||
// Spawn a main loop to handle all incoming messages.
|
||||
for {
|
||||
if err := h.handleMsg(p); err != nil {
|
||||
if err := h.handleMsg(p, payment); err != nil {
|
||||
p.Log().Debug("Light Ethereum message handling failed", "err", err)
|
||||
p.fcServer.DumpLogs()
|
||||
return err
|
||||
|
|
@ -143,7 +181,7 @@ func (h *clientHandler) handle(p *peer) error {
|
|||
|
||||
// handleMsg is invoked whenever an inbound message is received from a remote
|
||||
// peer. The remote connection is torn down upon returning any error.
|
||||
func (h *clientHandler) handleMsg(p *peer) error {
|
||||
func (h *clientHandler) handleMsg(p *peer, payment payment.Payment) error {
|
||||
// Read the next message from the remote peer, and ensure it's fully consumed
|
||||
msg, err := p.rw.ReadMsg()
|
||||
if err != nil {
|
||||
|
|
@ -308,6 +346,10 @@ func (h *clientHandler) handleMsg(p *peer) error {
|
|||
p.fcServer.ResumeFreeze(bv)
|
||||
p.freezeServer(false)
|
||||
p.Log().Debug("Service resumed")
|
||||
case PaymentResultMsg:
|
||||
if err := payment.Amend(msg.Payload); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
p.Log().Trace("Received invalid message", "code", msg.Code)
|
||||
return errResp(ErrInvalidMsgCode, "%v", msg.Code)
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ import (
|
|||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/les/payment"
|
||||
"github.com/ethereum/go-ethereum/light"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discv5"
|
||||
|
|
@ -65,6 +67,13 @@ type lesCommons struct {
|
|||
chtIndexer, bloomTrieIndexer *core.ChainIndexer
|
||||
oracle *checkpointOracle
|
||||
|
||||
// Payment channel relative fields
|
||||
paymentDb ethdb.Database // The database used to store all received payments or payment records
|
||||
paymentInited uint32 // The status indicator whether payment methods are allocated.
|
||||
address common.Address // The address used to pay or charge
|
||||
am *accounts.Manager // The global account manager which holds the local account
|
||||
channelManager *payment.PaymentChannelManager // Off-chain payment channel manager
|
||||
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
|
|
|||
41
les/payment/payment.go
Normal file
41
les/payment/payment.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package payment
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// Payment is the way the light client pays to the les server. Payment can be
|
||||
// implemented in many different ways, such as off-chain payment, on-chain payment.
|
||||
// All available payments must implement the following functions.
|
||||
type Payment interface {
|
||||
// Pay initiates a payment to the designated payee with specified
|
||||
// payemnt amount.
|
||||
Pay(amount *big.Int) error
|
||||
|
||||
// Receive receives a payment from the payer and returns any error
|
||||
// for payment processing and proving.
|
||||
Receive(msg io.Reader) error
|
||||
|
||||
// Amend amends the local payment db based on the received message.
|
||||
Amend(msg io.Reader) error
|
||||
|
||||
// Close exits the payment and opens the reqeust to withdraw all funds.
|
||||
Close() error
|
||||
}
|
||||
629
les/payment/payment_channel.go
Normal file
629
les/payment/payment_channel.go
Normal file
|
|
@ -0,0 +1,629 @@
|
|||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/contracts/accountbook"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
var errInvalidOpt = errors.New("invalid operation")
|
||||
|
||||
var (
|
||||
minimalDeposit = big.NewInt(1e6) // The minimal amount for single deposit operation, 1e6 gWei.
|
||||
minimalDepositThreshold = big.NewInt(2e5) // The minimal amount for triggering a new deposit, 2e5 gWei
|
||||
minimalCashThreshold = big.NewInt(1e6) // The minimal amount for triggering a cash operation, 1e6 gWei
|
||||
minimalChallengThreshold = big.NewInt(2e5) // The minimal amount for triggering a challenge, 2e5 gWei
|
||||
)
|
||||
|
||||
// DefaultPaymentChannelDraweeConfig is the default payment channel
|
||||
// config for drawee.
|
||||
var DefaultPaymentChannelDraweeConfig = &PaymentChannelConfig{
|
||||
Role: PaymentDrawee,
|
||||
AutoCash: true,
|
||||
|
||||
// The transaction fee of cash call is around 4e14 wei
|
||||
// The gas cost is around 60,000, we use 10GW as the price.
|
||||
// So a reasonable minimal call amount is 1e16 wei as well
|
||||
// as 1e-2 ether.
|
||||
AutoCashThreshold: big.NewInt(int64(1e7)), // 1e7 gWei as well as 1e-2 ether
|
||||
ChallengeThreshold: big.NewInt(int64(1e6)), // 1e6 gWei as well as 1e-3 ether
|
||||
}
|
||||
|
||||
// DefaultPaymentChannelDrawerConfig is the default payment channel
|
||||
// config for drawer.
|
||||
var DefaultPaymentChannelDrawerConfig = &PaymentChannelConfig{
|
||||
Role: PaymentDrawer,
|
||||
AutoDeposit: true,
|
||||
AutoClaim: true,
|
||||
|
||||
// The transaction fee of deposit call is around 4e14 wei
|
||||
// The gas cost is around 40,000, we use 10GW as the price.
|
||||
// So a reasonable minimal deposit amount is 1e16 wei as well
|
||||
// as 1e-2 ether.
|
||||
DepositAmount: big.NewInt(int64(1e7)), // 1e7 gWei as well as 1e-2 ether
|
||||
AutoDepositThreshold: big.NewInt(int64(1e6)), // 1e6 gWei as well as 1e-3 ether
|
||||
}
|
||||
|
||||
// PaymentRole is the role of user in payment channel.
|
||||
type PaymentRole int
|
||||
|
||||
const (
|
||||
PaymentDrawer PaymentRole = iota
|
||||
PaymentDrawee
|
||||
)
|
||||
|
||||
// PaymentChannelConfig defines all user-selectable options for both
|
||||
// drawer and drawee.
|
||||
type PaymentChannelConfig struct {
|
||||
// Role is the role of the user in the payment channel, either the
|
||||
// payer or the payee.
|
||||
Role PaymentRole
|
||||
|
||||
// Drawer relative options
|
||||
|
||||
// TrustedContract is a list of contract code hash which light client
|
||||
// can trust for usage. Light client users can configure or extend it
|
||||
// by themselves, but as default the in-built contract code hash is
|
||||
// included(for light clients).
|
||||
TrustedContracts []common.Hash
|
||||
|
||||
// DepositAmount is the amount deposited by each drawer for the deposit
|
||||
// operation. The unit is gWei. This option is only for drawer.
|
||||
DepositAmount *big.Int
|
||||
|
||||
// AutoDeposit is the indicator whether to perform automatic balance
|
||||
// recharge. This option is only for drawer.
|
||||
AutoDeposit bool
|
||||
|
||||
// AutoDepositThreshold is the threshold for the drawer to perform auto deposit.
|
||||
// When balance is below the threshold, auto deposit is triggered. This option is
|
||||
// only for drawer.
|
||||
AutoDepositThreshold *big.Int
|
||||
|
||||
// AutoClaim is the indicator whether to perform automatic claim.
|
||||
AutoClaim bool
|
||||
|
||||
// Drawee relative options
|
||||
|
||||
// AutoCash is an indicator that whether drawee to perform cheque cashing
|
||||
// automatically. This option is only for drawee.
|
||||
AutoCash bool
|
||||
|
||||
// AutoCashThreshold is the threshold for the drawee to perform auto cashing.
|
||||
// When accumulated received money from single drawer exceeds the threshold,
|
||||
// auto cashing is triggered. This option is only for drawee.
|
||||
AutoCashThreshold *big.Int
|
||||
|
||||
// ChallengeThreshold is the threshold for drawee to initiate the challenge to
|
||||
// the drawer's withdraw operation.
|
||||
//
|
||||
// If the drawer tries to withdraw the deposit that has been spent from the
|
||||
// contract, the drawee can initiate the challenge. But this needs to be done
|
||||
// through an on-chain transaction. So the drawer can choose to set the threshold
|
||||
// and initiate the challenge when the extracted amount exceeds the value.
|
||||
ChallengeThreshold *big.Int
|
||||
}
|
||||
|
||||
// sanitize checks the provided user configurations and changes anything that's
|
||||
// unreasonable or unworkable.
|
||||
func (config *PaymentChannelConfig) sanitize() *PaymentChannelConfig {
|
||||
conf := *config
|
||||
if conf.Role != PaymentDrawer && conf.Role != PaymentDrawee {
|
||||
return nil
|
||||
}
|
||||
if conf.Role == PaymentDrawer {
|
||||
// If auto deposit is disabled, return directly.
|
||||
if !conf.AutoDeposit {
|
||||
return &conf
|
||||
}
|
||||
if conf.DepositAmount == nil || conf.DepositAmount.Cmp(minimalDeposit) < 0 {
|
||||
log.Warn("Sanitizing invalid deposit amount", "provided(gWei)", conf.DepositAmount, "updated(gWei)", minimalDeposit)
|
||||
conf.DepositAmount = minimalDeposit
|
||||
}
|
||||
if conf.AutoDepositThreshold == nil || conf.AutoDepositThreshold.Cmp(minimalDepositThreshold) < 0 {
|
||||
log.Warn("Sanitizing invalid deposit threshold", "provided(gWei)", conf.AutoDepositThreshold, "updated(gWei)", minimalDepositThreshold)
|
||||
conf.AutoDepositThreshold = minimalDepositThreshold
|
||||
}
|
||||
} else {
|
||||
if conf.AutoCash && (conf.AutoCashThreshold == nil || conf.AutoCashThreshold.Cmp(minimalCashThreshold) < 0) {
|
||||
log.Warn("Sanitizing invalid cash threshold", "provided(gWei)", conf.AutoCashThreshold, "updated(gWei)", minimalCashThreshold)
|
||||
conf.AutoCashThreshold = minimalCashThreshold
|
||||
}
|
||||
if conf.ChallengeThreshold == nil || conf.ChallengeThreshold.Cmp(minimalChallengThreshold) < 0 {
|
||||
log.Warn("Sanitizing invalid challenge threshold", "provided(gWei)", conf.ChallengeThreshold, "updated(gWei)", minimalChallengThreshold)
|
||||
conf.ChallengeThreshold = minimalChallengThreshold
|
||||
}
|
||||
}
|
||||
return &conf
|
||||
}
|
||||
|
||||
// Peer defines all necessary method as the drawer or drawee.
|
||||
type Peer interface {
|
||||
// SendPayment sends the given cheque to the peer via network.
|
||||
SendPayment(cheque *accountbook.Cheque) error
|
||||
|
||||
// AddBalance notifies upper-level system we have received
|
||||
// the payment from the peer with specified amount.
|
||||
AddBalance(amount *big.Int) error
|
||||
}
|
||||
|
||||
// CurrentHeader retrieves the current header from the local chain.
|
||||
type ChainReader interface {
|
||||
CurrentHeader() *types.Header
|
||||
}
|
||||
|
||||
type PaymentChannel struct {
|
||||
config *PaymentChannelConfig
|
||||
chainReader ChainReader
|
||||
chanAddr common.Address
|
||||
peer Peer // The peer handler of counterparty
|
||||
drawer *accountbook.ChequeDrawer // Nil if payment is opened by drawee
|
||||
drawee *accountbook.ChequeDrawee // Nil if payment is opened by drawer
|
||||
|
||||
depositCh chan struct{}
|
||||
cashCh chan common.Address
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewPaymentChannel(config *PaymentChannelConfig, chainReader ChainReader, chanAddr common.Address, drawer *accountbook.ChequeDrawer, drawee *accountbook.ChequeDrawee, peer Peer) (*PaymentChannel, error) {
|
||||
// Sanitize the config to ensure all options are valid
|
||||
checked := config.sanitize()
|
||||
if checked == nil {
|
||||
return nil, errors.New("invalid config")
|
||||
}
|
||||
payment := &PaymentChannel{
|
||||
config: checked,
|
||||
chainReader: chainReader,
|
||||
chanAddr: chanAddr,
|
||||
peer: peer,
|
||||
drawer: drawer,
|
||||
drawee: drawee,
|
||||
depositCh: make(chan struct{}),
|
||||
cashCh: make(chan common.Address),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
if config.Role == PaymentDrawer {
|
||||
if config.AutoDeposit {
|
||||
payment.wg.Add(1)
|
||||
go payment.autoDeposit()
|
||||
}
|
||||
if config.AutoClaim {
|
||||
payment.wg.Add(1)
|
||||
go payment.autoClaim()
|
||||
}
|
||||
} else {
|
||||
if config.AutoCash {
|
||||
payment.wg.Add(1)
|
||||
go payment.autoCash()
|
||||
}
|
||||
payment.wg.Add(1)
|
||||
go payment.listenWithdraw()
|
||||
}
|
||||
return payment, nil
|
||||
}
|
||||
|
||||
// Pay initiates a payment to the designated payee with specified
|
||||
// payemnt amount and also trigger a deposit operation if auto deposit
|
||||
// is set and threshold is met.
|
||||
func (c *PaymentChannel) Pay(amount *big.Int) error {
|
||||
if c.config.Role != PaymentDrawer {
|
||||
return errInvalidOpt
|
||||
}
|
||||
cheque, unspent, err := c.drawer.IssueCheque(amount)
|
||||
if err != nil {
|
||||
if c.config.AutoDeposit && err == accountbook.ErrNotEnoughDeposit {
|
||||
select {
|
||||
case c.depositCh <- struct{}{}:
|
||||
case <-c.closeCh:
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
if c.config.AutoDeposit && unspent.Cmp(new(big.Int).Mul(c.config.AutoDepositThreshold, big.NewInt(params.GWei))) <= 0 {
|
||||
select {
|
||||
case c.depositCh <- struct{}{}:
|
||||
case <-c.closeCh:
|
||||
}
|
||||
}
|
||||
log.Info("Issued payment", "amount", amount, "channel", c.chanAddr)
|
||||
return c.peer.SendPayment(cheque)
|
||||
}
|
||||
|
||||
// Receive receives a payment from the payer and returns any error
|
||||
// for payment processing and proving.
|
||||
func (c *PaymentChannel) Receive(msg io.Reader) error {
|
||||
if c.config.Role != PaymentDrawee {
|
||||
return errInvalidOpt
|
||||
}
|
||||
var cheque accountbook.Cheque
|
||||
if err := rlp.Decode(msg, &cheque); err != nil {
|
||||
return err
|
||||
}
|
||||
amount, unpaid, err := c.drawee.AddCheque(&cheque)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.config.AutoCash && unpaid.Cmp(new(big.Int).Mul(c.config.AutoCashThreshold, big.NewInt(params.GWei))) >= 0 {
|
||||
select {
|
||||
case c.cashCh <- cheque.Drawer:
|
||||
case <-c.closeCh:
|
||||
}
|
||||
}
|
||||
return c.peer.AddBalance(amount)
|
||||
}
|
||||
|
||||
// Amend amends the local cheque db of drawer with externally provided cheque
|
||||
// signed by drawer itself.
|
||||
func (c *PaymentChannel) Amend(msg io.Reader) error {
|
||||
if c.config.Role != PaymentDrawer {
|
||||
return errInvalidOpt
|
||||
}
|
||||
var serr accountbook.StaleChequeError
|
||||
if err := rlp.Decode(msg, &serr); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.drawer.Amend(serr.Evidence)
|
||||
}
|
||||
|
||||
// Close exits the payment and opens the reqeust to withdraw all funds.
|
||||
func (c *PaymentChannel) Close() error {
|
||||
if c.config.Role != PaymentDrawer {
|
||||
return errInvalidOpt
|
||||
}
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancelFn()
|
||||
|
||||
if err := c.drawer.Withdraw(ctx); err != nil {
|
||||
log.Info("Failed to open withdraw request", "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PaymentChannel) autoCash() {
|
||||
log.Info("Enable auto cash", "channel", c.chanAddr, "threshold", c.config.AutoCashThreshold)
|
||||
defer c.wg.Done()
|
||||
|
||||
var (
|
||||
done chan struct{} // Non-nil if cash routine is active.
|
||||
cash = func(drawer common.Address, done chan struct{}) {
|
||||
defer func() { done <- struct{}{} }()
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancelFn()
|
||||
|
||||
if err := c.drawee.Cash(ctx, drawer, true); err != nil {
|
||||
log.Info("Failed to cash payment", "drawer", drawer, "error", err)
|
||||
} else {
|
||||
log.Info("Succeed to cash payment", "drawer", drawer)
|
||||
}
|
||||
}
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case addr := <-c.cashCh:
|
||||
if done == nil {
|
||||
done = make(chan struct{})
|
||||
go cash(addr, done)
|
||||
}
|
||||
case <-done:
|
||||
done = nil
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PaymentChannel) autoDeposit() {
|
||||
log.Info("Enable auto deposit", "channel", c.chanAddr, "amount", c.config.DepositAmount)
|
||||
defer c.wg.Done()
|
||||
|
||||
var (
|
||||
done chan struct{} // Non-nil if deposit routine is active.
|
||||
deposit = func(done chan struct{}) {
|
||||
defer func() { done <- struct{}{} }()
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancelFn()
|
||||
|
||||
status, err := c.drawer.Deposit(ctx, new(big.Int).Mul(c.config.DepositAmount, big.NewInt(params.GWei)))
|
||||
if err != nil || !status {
|
||||
log.Info("Failed to deposit", "channel", c.chanAddr, "amount(gWei)", c.config.DepositAmount, "error", err)
|
||||
} else {
|
||||
log.Info("Succeed to deposit", "channel", c.chanAddr, "amount(gWei)", c.config.DepositAmount)
|
||||
}
|
||||
}
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case <-c.depositCh:
|
||||
if done == nil {
|
||||
done = make(chan struct{})
|
||||
go deposit(done)
|
||||
}
|
||||
case <-done:
|
||||
done = nil
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PaymentChannel) autoClaim() {
|
||||
defer c.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
var (
|
||||
done chan struct{} // Non-nil if deposit routine is active.
|
||||
claim = func(done chan struct{}) {
|
||||
defer func() { done <- struct{}{} }()
|
||||
defer c.drawer.ResetWithdrawlRecord()
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancelFn()
|
||||
|
||||
if status, err := c.drawer.Claim(ctx); err != nil || !status {
|
||||
log.Info("Failed to claim", "channel", c.chanAddr, "error", err)
|
||||
} else {
|
||||
log.Info("Succeed to claim", "channel", c.chanAddr)
|
||||
}
|
||||
}
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if done != nil {
|
||||
continue
|
||||
}
|
||||
createdAt := c.drawer.WithdrawalRecord()
|
||||
if createdAt != 0 {
|
||||
local := c.chainReader.CurrentHeader()
|
||||
if local.Number.Uint64() > createdAt && local.Number.Uint64()-createdAt > accountbook.ChallengeTimeWindow {
|
||||
done = make(chan struct{})
|
||||
go claim(done)
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
done = nil
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PaymentChannel) listenWithdraw() {
|
||||
defer c.wg.Done()
|
||||
|
||||
sub, channel, err := c.drawee.ListenWithdraw()
|
||||
if err != nil {
|
||||
log.Info("Failed to subscribe withdraw event", "error", err)
|
||||
return
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-channel:
|
||||
amount := event.Amount
|
||||
if amount == nil || amount.Cmp(new(big.Int).Mul(c.config.ChallengeThreshold, big.NewInt(params.GWei))) < 0 {
|
||||
continue
|
||||
}
|
||||
_, err := c.drawee.Unspent(event.Addr)
|
||||
if err != nil && err != accountbook.ErrNotEnoughDeposit {
|
||||
continue
|
||||
}
|
||||
// Drawer tries to withdraw spent money, challenge him.
|
||||
go func() {
|
||||
if err := c.drawee.Cash(context.Background(), event.Addr, false); err != nil {
|
||||
log.Info("Failed to challenge", "drawer", event.Addr, "error", err)
|
||||
} else {
|
||||
log.Info("Succeed to challenge", "drawer", event.Addr, "error", err)
|
||||
}
|
||||
}()
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PaymentChannel) exit() {
|
||||
close(c.closeCh)
|
||||
c.wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
type PaymentChannelManager struct {
|
||||
config *PaymentChannelConfig
|
||||
chainReader ChainReader
|
||||
localAddr common.Address
|
||||
txSigner *bind.TransactOpts
|
||||
chequeSigner func(data []byte) ([]byte, error)
|
||||
db ethdb.Database
|
||||
lock sync.RWMutex
|
||||
|
||||
// payments are all established channels. For payment drawer, the key of payment
|
||||
// map is channel contract address, otherwise the key refers to drawer's address.
|
||||
payments map[common.Address]*PaymentChannel
|
||||
drawee *accountbook.ChequeDrawee // Nil if manager is opened by drawer
|
||||
|
||||
// Backends used to interact with the underlying payment contract
|
||||
cBackend bind.ContractBackend
|
||||
dBackend bind.DeployBackend
|
||||
}
|
||||
|
||||
// NewPaymentChannel initializes a one-to-one payment channel for both
|
||||
// drawer and drawee.
|
||||
func NewPaymentChannelManager(config *PaymentChannelConfig, chainReader ChainReader, txSigner *bind.TransactOpts, chequeSigner func(digestHash []byte) ([]byte, error), localAddr common.Address, cBackend bind.ContractBackend, dBackend bind.DeployBackend, db ethdb.Database) (*PaymentChannelManager, error) {
|
||||
c := &PaymentChannelManager{
|
||||
config: config,
|
||||
chainReader: chainReader,
|
||||
localAddr: localAddr,
|
||||
txSigner: txSigner,
|
||||
chequeSigner: chequeSigner,
|
||||
db: db,
|
||||
cBackend: cBackend,
|
||||
dBackend: dBackend,
|
||||
payments: make(map[common.Address]*PaymentChannel),
|
||||
}
|
||||
// Drawer has to initialize channel here if contract
|
||||
// hasn't been deployed yet.
|
||||
if c.config.Role == PaymentDrawee {
|
||||
drawee, err := accountbook.NewChequeDrawee(c.localAddr, txSigner, cBackend, dBackend, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.drawee = drawee
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// OpenChannel establishes a new payment channel for new customer or new vendor.
|
||||
// If we are payment drawer, the addr refers to the payment channel contract addr,
|
||||
// otherwise, the addr refers to drawer's address.
|
||||
func (c *PaymentChannelManager) OpenChannel(addr common.Address, peer Peer) (Payment, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Filter all duplicated channels.
|
||||
if _, exist := c.payments[addr]; exist {
|
||||
return nil, errors.New("duplicated payment channel")
|
||||
}
|
||||
var err error
|
||||
var channel *PaymentChannel
|
||||
if c.config.Role == PaymentDrawer {
|
||||
// We are payment drawer, establish a outgoing channel with
|
||||
// specified contract address and counterparty peer.
|
||||
drawer, err := accountbook.NewChequeDrawer(c.txSigner, c.chequeSigner, c.localAddr, addr, c.cBackend, c.dBackend, c.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channel, err = NewPaymentChannel(c.config, c.chainReader, addr, drawer, nil, peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.payments[addr] = channel
|
||||
} else {
|
||||
// We are payment drawee, establish a incoming channel with
|
||||
// specified counterparty address and peer.
|
||||
channel, err = NewPaymentChannel(c.config, c.chainReader, c.drawee.ContractAddr(), nil, c.drawee, peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.payments[addr] = channel
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// CloseChannel closes a channel with given address. If we are payment drawer,
|
||||
// the addr refers to the payment channel contract addr, otherwise, the addr
|
||||
// refers to drawer's address.
|
||||
func (c *PaymentChannelManager) CloseChannel(addr common.Address) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if payment, exist := c.payments[addr]; !exist {
|
||||
return errors.New("channel doesn't exist")
|
||||
} else {
|
||||
payment.exit()
|
||||
delete(c.payments, addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyChannel ensures the code of payment channel is trusted.
|
||||
func (c *PaymentChannelManager) VerifyChannel(chanAddr common.Address) error {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
if c.config.Role == PaymentDrawee {
|
||||
return nil
|
||||
}
|
||||
if len(c.config.TrustedContracts) == 0 {
|
||||
return nil
|
||||
}
|
||||
payment, exist := c.payments[chanAddr]
|
||||
if !exist {
|
||||
return errors.New("channel doesn't exist")
|
||||
}
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancelFn()
|
||||
hash, err := payment.drawer.CodeHash(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, h := range c.config.TrustedContracts {
|
||||
if h == hash {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("untrusted contract")
|
||||
}
|
||||
|
||||
// ChannelAddress returns all established channel addresses.
|
||||
func (c *PaymentChannelManager) ChannelAddresses() []common.Address {
|
||||
if c.config.Role == PaymentDrawer {
|
||||
var addresses []common.Address
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
for addr := range c.payments {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
return []common.Address{c.drawee.ContractAddr()}
|
||||
}
|
||||
|
||||
// ChannelInfo includes all basic information about the specified channel.
|
||||
type ChannelInfo struct {
|
||||
Received []*accountbook.Cheque
|
||||
Payed *big.Int
|
||||
}
|
||||
|
||||
// ChannelInfo returns all basic information about the channel.
|
||||
func (c *PaymentChannelManager) ChannelInfo(chanAddr common.Address) ChannelInfo {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
var info ChannelInfo
|
||||
if c.config.Role == PaymentDrawer {
|
||||
payment, exist := c.payments[chanAddr]
|
||||
if !exist {
|
||||
return info
|
||||
}
|
||||
info.Payed = payment.drawer.Payed()
|
||||
} else {
|
||||
info.Received = c.drawee.ListCheques()
|
||||
}
|
||||
return info
|
||||
}
|
||||
30
les/peer.go
30
les/peer.go
|
|
@ -28,6 +28,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/contracts/accountbook"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
|
|
@ -117,6 +118,10 @@ type peer struct {
|
|||
onlyAnnounce bool
|
||||
chainSince, chainRecent uint64
|
||||
stateSince, stateRecent uint64
|
||||
|
||||
// Payment relative fields
|
||||
paymentChannel common.Address
|
||||
payerAddr common.Address
|
||||
}
|
||||
|
||||
func newPeer(version int, network uint64, trusted bool, p *p2p.Peer, rw p2p.MsgReadWriter) *peer {
|
||||
|
|
@ -398,6 +403,21 @@ func (p *peer) SendResume(bv uint64) error {
|
|||
return p2p.Send(p.rw, ResumeMsg, bv)
|
||||
}
|
||||
|
||||
// SendPayment sends a signed cheque to this peer.
|
||||
func (p *peer) SendPayment(cheque *accountbook.Cheque) error {
|
||||
return p2p.Send(p.rw, PaymentMsg, cheque)
|
||||
}
|
||||
|
||||
func (p *peer) SendPaymentResult(res *accountbook.StaleChequeError) error {
|
||||
return p2p.Send(p.rw, PaymentResultMsg, res)
|
||||
}
|
||||
|
||||
// Only for debugging
|
||||
func (p *peer) AddBalance(amount *big.Int) error {
|
||||
fmt.Printf("[Add balance] add %d for %s\n", amount, p.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplyBlockHeaders creates a reply with a batch of block headers
|
||||
func (p *peer) ReplyBlockHeaders(reqID uint64, headers []*types.Header) *reply {
|
||||
data, _ := rlp.EncodeToBytes(headers)
|
||||
|
|
@ -572,7 +592,7 @@ func (p *peer) sendReceiveHandshake(sendList keyValueList) (keyValueList, error)
|
|||
|
||||
// Handshake executes the les protocol handshake, negotiating version number,
|
||||
// network IDs, difficulties, head and genesis blocks.
|
||||
func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis common.Hash, server *LesServer) error {
|
||||
func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis common.Hash, server *LesServer, addr common.Address) error {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
|
|
@ -623,6 +643,10 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis
|
|||
send = send.add("checkpoint/registerHeight", height)
|
||||
}
|
||||
}
|
||||
// If payment is enabled, add the payment info in the handshake packet
|
||||
if atomic.LoadUint32(&server.paymentInited) == 1 {
|
||||
send = send.add("payment/paymentChannel", server.channelManager.ChannelAddresses()[0])
|
||||
}
|
||||
} else {
|
||||
// Add some client-specific handshake fields
|
||||
p.announceType = announceTypeSimple
|
||||
|
|
@ -630,6 +654,7 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis
|
|||
p.announceType = announceTypeSigned
|
||||
}
|
||||
send = send.add("announceType", p.announceType)
|
||||
send = send.add("payment/payer", addr)
|
||||
}
|
||||
|
||||
recvList, err := p.sendReceiveHandshake(send)
|
||||
|
|
@ -680,6 +705,7 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis
|
|||
p.announceType = announceTypeSimple
|
||||
}
|
||||
p.fcClient = flowcontrol.NewClientNode(server.fcManager, server.defParams)
|
||||
recv.get("payment/payer", &p.payerAddr)
|
||||
} else {
|
||||
if recv.get("serveChainSince", &p.chainSince) != nil {
|
||||
p.onlyAnnounce = true
|
||||
|
|
@ -719,6 +745,8 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis
|
|||
recv.get("checkpoint/value", &p.checkpoint)
|
||||
recv.get("checkpoint/registerHeight", &p.checkpointNumber)
|
||||
|
||||
recv.get("payment/paymentChannel", &p.paymentChannel)
|
||||
|
||||
if !p.onlyAnnounce {
|
||||
for msgCode := range reqAvgTimeCost {
|
||||
if p.fcCosts[msgCode] == nil {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ func TestPeerHandshakeSetAnnounceTypeToAnnounceTypeSignedForTrustedPeer(t *testi
|
|||
},
|
||||
network: NetworkId,
|
||||
}
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil)
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
|
||||
if err != nil {
|
||||
t.Fatalf("Handshake error: %s", err)
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ func TestPeerHandshakeAnnounceTypeSignedForTrustedPeersPeerNotInTrusted(t *testi
|
|||
},
|
||||
network: NetworkId,
|
||||
}
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil)
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ func TestPeerHandshakeDefaultAllRequests(t *testing.T) {
|
|||
network: NetworkId,
|
||||
}
|
||||
|
||||
err := p.Handshake(td, hash, headNum, genesis, s)
|
||||
err := p.Handshake(td, hash, headNum, genesis, s, common.Address{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -188,7 +188,7 @@ func TestPeerHandshakeServerSendOnlyAnnounceRequestsHeaders(t *testing.T) {
|
|||
network: NetworkId,
|
||||
}
|
||||
|
||||
err := p.Handshake(td, hash, headNum, genesis, s)
|
||||
err := p.Handshake(td, hash, headNum, genesis, s, common.Address{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -214,7 +214,7 @@ func TestPeerHandshakeClientReceiveOnlyAnnounceRequestsHeaders(t *testing.T) {
|
|||
trusted: true,
|
||||
}
|
||||
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil)
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ func TestPeerHandshakeClientReturnErrorOnUselessPeer(t *testing.T) {
|
|||
network: NetworkId,
|
||||
}
|
||||
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil)
|
||||
err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
|
||||
if err == nil {
|
||||
t.FailNow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,17 +33,18 @@ import (
|
|||
const (
|
||||
lpv2 = 2
|
||||
lpv3 = 3
|
||||
lpv4 = 4
|
||||
)
|
||||
|
||||
// Supported versions of the les protocol (first is primary)
|
||||
var (
|
||||
ClientProtocolVersions = []uint{lpv2, lpv3}
|
||||
ServerProtocolVersions = []uint{lpv2, lpv3}
|
||||
ClientProtocolVersions = []uint{lpv2, lpv3, lpv4}
|
||||
ServerProtocolVersions = []uint{lpv2, lpv3, lpv4}
|
||||
AdvertiseProtocolVersions = []uint{lpv2} // clients are searching for the first advertised protocol in the list
|
||||
)
|
||||
|
||||
// Number of implemented message corresponding to different protocol versions.
|
||||
var ProtocolLengths = map[uint]uint64{lpv2: 22, lpv3: 24}
|
||||
var ProtocolLengths = map[uint]uint64{lpv2: 22, lpv3: 24, lpv4: 26}
|
||||
|
||||
const (
|
||||
NetworkId = 1
|
||||
|
|
@ -74,6 +75,9 @@ const (
|
|||
// Protocol messages introduced in LPV3
|
||||
StopMsg = 0x16
|
||||
ResumeMsg = 0x17
|
||||
// Protocol messages introduced in LPV4
|
||||
PaymentMsg = 0x18
|
||||
PaymentResultMsg = 0x19
|
||||
)
|
||||
|
||||
type requestInfo struct {
|
||||
|
|
|
|||
|
|
@ -18,15 +18,20 @@ package les
|
|||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/les/flowcontrol"
|
||||
"github.com/ethereum/go-ethereum/les/payment"
|
||||
"github.com/ethereum/go-ethereum/light"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discv5"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
|
@ -42,6 +47,7 @@ type LesServer struct {
|
|||
handler *serverHandler
|
||||
lesTopics []discv5.Topic
|
||||
privateKey *ecdsa.PrivateKey
|
||||
synced func() bool
|
||||
|
||||
// Flow control and capacity management
|
||||
fcManager *flowcontrol.ClientManager
|
||||
|
|
@ -55,7 +61,7 @@ type LesServer struct {
|
|||
threadsBusy int // Request serving threads count when system is busy(block insertion).
|
||||
}
|
||||
|
||||
func NewLesServer(e *eth.Ethereum, config *eth.Config) (*LesServer, error) {
|
||||
func NewLesServer(ctx *node.ServiceContext, e *eth.Ethereum, config *eth.Config) (*LesServer, error) {
|
||||
// Collect les protocol version information supported by local node.
|
||||
lesTopics := make([]discv5.Topic, len(AdvertiseProtocolVersions))
|
||||
for i, pv := range AdvertiseProtocolVersions {
|
||||
|
|
@ -79,6 +85,7 @@ func NewLesServer(e *eth.Ethereum, config *eth.Config) (*LesServer, error) {
|
|||
chtIndexer: light.NewChtIndexer(e.ChainDb(), nil, params.CHTFrequency, params.HelperTrieProcessConfirmations),
|
||||
bloomTrieIndexer: light.NewBloomTrieIndexer(e.ChainDb(), nil, params.BloomBitsBlocks, params.BloomTrieFrequency),
|
||||
closeCh: make(chan struct{}),
|
||||
am: ctx.AccountManager,
|
||||
},
|
||||
archiveMode: e.ArchiveMode(),
|
||||
lesTopics: lesTopics,
|
||||
|
|
@ -86,6 +93,7 @@ func NewLesServer(e *eth.Ethereum, config *eth.Config) (*LesServer, error) {
|
|||
servingQueue: newServingQueue(int64(time.Millisecond*10), float64(config.LightServ)/100),
|
||||
threadsBusy: config.LightServ/100 + 1,
|
||||
threadsIdle: threads,
|
||||
synced: e.Synced,
|
||||
}
|
||||
srv.handler = newServerHandler(srv, e.BlockChain(), e.ChainDb(), e.TxPool(), e.Synced)
|
||||
srv.costTracker, srv.minCapacity = newCostTracker(e.ChainDb(), config)
|
||||
|
|
@ -123,6 +131,15 @@ func NewLesServer(e *eth.Ethereum, config *eth.Config) (*LesServer, error) {
|
|||
"chtroot", checkpoint.CHTRoot, "bloomroot", checkpoint.BloomRoot)
|
||||
}
|
||||
srv.chtIndexer.Start(e.BlockChain())
|
||||
|
||||
if config.LightServiceCharge {
|
||||
paymentDb, err := ctx.OpenDatabase("paymentdata", 0, 0, "eth/db/paymentdata") // How to disable metrics?
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srv.paymentDb = paymentDb
|
||||
srv.address = config.LightAddress
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +220,9 @@ func (s *LesServer) Stop() {
|
|||
|
||||
// Note, bloom trie indexer is closed by parent bloombits indexer.
|
||||
s.chtIndexer.Close()
|
||||
if s.paymentDb != nil {
|
||||
s.paymentDb.Close()
|
||||
}
|
||||
s.wg.Wait()
|
||||
log.Info("Les server stopped")
|
||||
}
|
||||
|
|
@ -212,11 +232,39 @@ func (s *LesServer) SetBloomBitsIndexer(bloomIndexer *core.ChainIndexer) {
|
|||
}
|
||||
|
||||
// SetClient sets the rpc client and starts running checkpoint contract if it is not yet watched.
|
||||
func (s *LesServer) SetContractBackend(backend bind.ContractBackend) {
|
||||
if s.oracle == nil {
|
||||
func (s *LesServer) SetBackends(cbackend bind.ContractBackend, dbackend bind.DeployBackend) {
|
||||
if s.oracle != nil {
|
||||
s.oracle.start(cbackend)
|
||||
}
|
||||
if s.config.LightServiceCharge {
|
||||
go func() {
|
||||
if s.address == (common.Address{}) {
|
||||
log.Warn("Failed to setup cheque drawee", "error", "empty cheque drawee address")
|
||||
return
|
||||
}
|
||||
s.oracle.start(backend)
|
||||
for {
|
||||
if !s.synced() {
|
||||
time.Sleep(time.Second * 10)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
account := accounts.Account{Address: s.address}
|
||||
wallet, err := s.am.Find(account)
|
||||
if err != nil {
|
||||
log.Warn("Failed to setup cheque drawee", "error", err)
|
||||
return
|
||||
}
|
||||
channelManager, err := payment.NewPaymentChannelManager(payment.DefaultPaymentChannelDraweeConfig, s.chainReader, bind.NewRawTransactor(wallet.SignTx, account), nil, s.address, cbackend, dbackend, s.paymentDb)
|
||||
if err != nil {
|
||||
log.Warn("Failed to setup cheque drawee", "error", err)
|
||||
return
|
||||
}
|
||||
s.channelManager = channelManager
|
||||
atomic.StoreUint32(&s.paymentInited, 1) // Mark payment channel is available now
|
||||
log.Info("Succeed to setup cheque drawee", "address", s.address)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// capacityManagement starts an event handler loop that updates the recharge curve of
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/contracts/accountbook"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
|
|
@ -31,6 +33,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/les/payment"
|
||||
"github.com/ethereum/go-ethereum/light"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
|
|
@ -114,6 +117,9 @@ func (h *serverHandler) handle(p *peer) error {
|
|||
}
|
||||
p.Log().Debug("Light Ethereum peer connected", "name", p.Name())
|
||||
|
||||
if h.server.config.LightServiceCharge && atomic.LoadUint32(&h.server.paymentInited) == 0 {
|
||||
return errors.New("payment hasn't been initialized")
|
||||
}
|
||||
// Execute the LES handshake
|
||||
var (
|
||||
head = h.blockchain.CurrentHeader()
|
||||
|
|
@ -121,7 +127,7 @@ func (h *serverHandler) handle(p *peer) error {
|
|||
number = head.Number.Uint64()
|
||||
td = h.blockchain.GetTd(hash, number)
|
||||
)
|
||||
if err := p.Handshake(td, hash, number, h.blockchain.Genesis().Hash(), h.server); err != nil {
|
||||
if err := p.Handshake(td, hash, number, h.blockchain.Genesis().Hash(), h.server, common.Address{}); err != nil {
|
||||
p.Log().Debug("Light Ethereum handshake failed", "err", err)
|
||||
return err
|
||||
}
|
||||
|
|
@ -150,7 +156,16 @@ func (h *serverHandler) handle(p *peer) error {
|
|||
clientConnectionGauge.Update(int64(h.server.peers.Len()))
|
||||
connectionTimer.Update(time.Duration(mclock.Now() - connectedAt))
|
||||
}()
|
||||
|
||||
// Open the channel if client wants to pay
|
||||
var payment payment.Payment
|
||||
if p.payerAddr != (common.Address{}) {
|
||||
c, err := h.server.channelManager.OpenChannel(p.payerAddr, p)
|
||||
if err != nil {
|
||||
p.Log().Error("Failed to open channel", "error", err)
|
||||
}
|
||||
defer h.server.channelManager.CloseChannel(p.payerAddr)
|
||||
payment = c
|
||||
}
|
||||
// Spawn a main loop to handle all incoming messages.
|
||||
for {
|
||||
select {
|
||||
|
|
@ -159,7 +174,7 @@ func (h *serverHandler) handle(p *peer) error {
|
|||
return err
|
||||
default:
|
||||
}
|
||||
if err := h.handleMsg(p, &wg); err != nil {
|
||||
if err := h.handleMsg(p, &wg, payment); err != nil {
|
||||
p.Log().Debug("Light Ethereum message handling failed", "err", err)
|
||||
return err
|
||||
}
|
||||
|
|
@ -168,7 +183,7 @@ func (h *serverHandler) handle(p *peer) error {
|
|||
|
||||
// handleMsg is invoked whenever an inbound message is received from a remote
|
||||
// peer. The remote connection is torn down upon returning any error.
|
||||
func (h *serverHandler) handleMsg(p *peer, wg *sync.WaitGroup) error {
|
||||
func (h *serverHandler) handleMsg(p *peer, wg *sync.WaitGroup, paymentMethod payment.Payment) error {
|
||||
// Read the next message from the remote peer, and ensure it's fully consumed
|
||||
msg, err := p.rw.ReadMsg()
|
||||
if err != nil {
|
||||
|
|
@ -817,7 +832,14 @@ func (h *serverHandler) handleMsg(p *peer, wg *sync.WaitGroup) error {
|
|||
}
|
||||
}()
|
||||
}
|
||||
|
||||
case PaymentMsg:
|
||||
if err := paymentMethod.Receive(msg.Payload); err != nil {
|
||||
if serr, ok := err.(*accountbook.StaleChequeError); ok {
|
||||
p.SendPaymentResult(serr)
|
||||
}
|
||||
p.Log().Error("Failed to receive payment", "error", err)
|
||||
return err
|
||||
}
|
||||
default:
|
||||
p.Log().Trace("Received invalid message", "code", msg.Code)
|
||||
clientErrorMeter.Mark(1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue