diff --git a/accounts/abi/bind/auth.go b/accounts/abi/bind/auth.go
index e51f0bd8ea..16517c35a1 100644
--- a/accounts/abi/bind/auth.go
+++ b/accounts/abi/bind/auth.go
@@ -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
+ },
+ }
+}
diff --git a/accounts/external/backend.go b/accounts/external/backend.go
index 6089ca9844..9b6b0200dd 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -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
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index 7cfa3e8adf..443e117ac2 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -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() {
diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go
index b3b6b5f93d..f490b4746c 100644
--- a/cmd/geth/usage.go
+++ b/cmd/geth/usage.go
@@ -94,6 +94,9 @@ var AppHelpFlagGroups = []flagGroup{
utils.UltraLightServersFlag,
utils.UltraLightFractionFlag,
utils.UltraLightOnlyAnnounceFlag,
+ utils.ServiceChargeFlag,
+ utils.ServicePaymentFlag,
+ utils.LightAddressFlag,
},
},
{
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index c6846d312b..c33cf17666 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -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
diff --git a/contracts/accountbook/accountbook.go b/contracts/accountbook/accountbook.go
new file mode 100644
index 0000000000..2d22b77e75
--- /dev/null
+++ b/contracts/accountbook/accountbook.go
@@ -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 .
+
+// 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
+}
diff --git a/contracts/accountbook/chequedb.go b/contracts/accountbook/chequedb.go
new file mode 100644
index 0000000000..cda77074a8
--- /dev/null
+++ b/contracts/accountbook/chequedb.go
@@ -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 .
+
+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
+}
diff --git a/contracts/accountbook/chequedb_test.go b/contracts/accountbook/chequedb_test.go
new file mode 100644
index 0000000000..6a1a2cc591
--- /dev/null
+++ b/contracts/accountbook/chequedb_test.go
@@ -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 .
+
+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")
+ }
+}
diff --git a/contracts/accountbook/contract/accountbook.go b/contracts/accountbook/contract/accountbook.go
new file mode 100644
index 0000000000..ee9fc04d74
--- /dev/null
+++ b/contracts/accountbook/contract/accountbook.go
@@ -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
+}
diff --git a/contracts/accountbook/contract/accountbook.sol b/contracts/accountbook/contract/accountbook.sol
new file mode 100644
index 0000000000..c9da562909
--- /dev/null
+++ b/contracts/accountbook/contract/accountbook.sol
@@ -0,0 +1,212 @@
+pragma solidity ^0.5.12;
+
+/**
+ * @title AccountBook
+ * @author Gary Rong
+ * @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;
+}
diff --git a/contracts/accountbook/contract/accountbook_test.go b/contracts/accountbook/contract/accountbook_test.go
new file mode 100644
index 0000000000..ae5a395a75
--- /dev/null
+++ b/contracts/accountbook/contract/accountbook_test.go
@@ -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 .
+
+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)
+}
diff --git a/contracts/accountbook/drawee.go b/contracts/accountbook/drawee.go
new file mode 100644
index 0000000000..d9acf42c9b
--- /dev/null
+++ b/contracts/accountbook/drawee.go
@@ -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 .
+
+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())
+}
diff --git a/contracts/accountbook/drawee_test.go b/contracts/accountbook/drawee_test.go
new file mode 100644
index 0000000000..fcefdd23b4
--- /dev/null
+++ b/contracts/accountbook/drawee_test.go
@@ -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 .
+
+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))
+}
diff --git a/contracts/accountbook/drawer.go b/contracts/accountbook/drawer.go
new file mode 100644
index 0000000000..68cff98c71
--- /dev/null
+++ b/contracts/accountbook/drawer.go
@@ -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 .
+
+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())
+}
diff --git a/contracts/accountbook/drawer_test.go b/contracts/accountbook/drawer_test.go
new file mode 100644
index 0000000000..4f5932f836
--- /dev/null
+++ b/contracts/accountbook/drawer_test.go
@@ -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 .
+
+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)
+}
diff --git a/contracts/accountbook/errors.go b/contracts/accountbook/errors.go
new file mode 100644
index 0000000000..f9b67b3226
--- /dev/null
+++ b/contracts/accountbook/errors.go
@@ -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 .
+
+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
+}
diff --git a/eth/backend.go b/eth/backend.go
index 83e05e96a8..2c308d4704 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -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)
}
}
diff --git a/eth/config.go b/eth/config.go
index 5094a533bf..ff3d6d578e 100644
--- a/eth/config.go
+++ b/eth/config.go
@@ -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:"-"`
diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go
index dbffbd2a83..b8b3f8d307 100644
--- a/internal/web3ext/web3ext.go
+++ b/internal/web3ext/web3ext.go
@@ -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'
+ }),
]
});
`
diff --git a/les/api.go b/les/api.go
index ad511c9d6b..6ca3059b6c 100644
--- a/les/api.go
+++ b/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
+}
diff --git a/les/api_backend.go b/les/api_backend.go
index e01e1be98b..29225bcd61 100644
--- a/les/api_backend.go
+++ b/les/api_backend.go
@@ -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 {
diff --git a/les/api_test.go b/les/api_test.go
index 06a519b62b..f5fecfbcc2 100644
--- a/les/api_test.go
+++ b/les/api_test.go
@@ -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
}
diff --git a/les/benchmark.go b/les/benchmark.go
index 42eeef10f3..89bf156c2e 100644
--- a/les/benchmark.go
+++ b/les/benchmark.go
@@ -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
}
diff --git a/les/client.go b/les/client.go
index 1ad44e16d7..c459c89a43 100644
--- a/les/client.go
+++ b/les/client.go
@@ -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"
@@ -60,11 +62,10 @@ type LightEthereum struct {
bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests
bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports
- ApiBackend *LesApiBackend
- eventMux *event.TypeMux
- engine consensus.Engine
- accountManager *accounts.Manager
- netRPCService *ethapi.PublicNetAPI
+ ApiBackend *LesApiBackend
+ eventMux *event.TypeMux
+ engine consensus.Engine
+ netRPCService *ethapi.PublicNetAPI
}
func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
@@ -88,14 +89,14 @@ 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),
- serverPool: newServerPool(chainDb, config.UltraLightServers),
+ eventMux: ctx.EventMux,
+ reqDist: newRequestDistributor(peers, &mclock.System{}),
+ engine: eth.CreateConsensusEngine(ctx, chainConfig, &config.Ethash, nil, false, chainDb),
+ bloomRequests: make(chan chan *bloombits.Retrieval),
+ bloomIndexer: eth.NewBloomIndexer(chainDb, params.BloomBitsBlocksClient, params.HelperTrieConfirmations),
+ serverPool: newServerPool(chainDb, config.UltraLightServers),
}
leth.retriever = newRetrieveManager(peers, leth.reqDist, leth.serverPool)
leth.relay = newLesTxRelay(peers, leth.retriever)
@@ -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 {
- return
+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
+ }
+ 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)
+ }()
}
- s.oracle.start(backend)
}
diff --git a/les/client_handler.go b/les/client_handler.go
index 7fdb165719..2fc890ee22 100644
--- a/les/client_handler.go
+++ b/les/client_handler.go
@@ -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)
diff --git a/les/commons.go b/les/commons.go
index ad3c5aef3d..37ba33f162 100644
--- a/les/commons.go
+++ b/les/commons.go
@@ -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
}
diff --git a/les/payment/payment.go b/les/payment/payment.go
new file mode 100644
index 0000000000..96a590b901
--- /dev/null
+++ b/les/payment/payment.go
@@ -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 .
+
+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
+}
diff --git a/les/payment/payment_channel.go b/les/payment/payment_channel.go
new file mode 100644
index 0000000000..e37247d498
--- /dev/null
+++ b/les/payment/payment_channel.go
@@ -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 .
+
+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
+}
diff --git a/les/peer.go b/les/peer.go
index ab5b30a657..f4cdf4f55f 100644
--- a/les/peer.go
+++ b/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 {
diff --git a/les/peer_test.go b/les/peer_test.go
index db74a052c1..c1c81a66fb 100644
--- a/les/peer_test.go
+++ b/les/peer_test.go
@@ -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()
}
diff --git a/les/protocol.go b/les/protocol.go
index 36af88aea6..e39f8c842d 100644
--- a/les/protocol.go
+++ b/les/protocol.go
@@ -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 {
diff --git a/les/server.go b/les/server.go
index e68903dd81..21ab3585ae 100644
--- a/les/server.go
+++ b/les/server.go
@@ -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 {
- return
+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
+ }
+ 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)
+ }()
}
- s.oracle.start(backend)
}
// capacityManagement starts an event handler loop that updates the recharge curve of
diff --git a/les/server_handler.go b/les/server_handler.go
index 16249ef1ba..cc51fe8ee3 100644
--- a/les/server_handler.go
+++ b/les/server_handler.go
@@ -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)