contracts/accountbook: implement accountbook contract

This commit is contained in:
rjl493456442 2019-11-11 20:34:47 +08:00
parent 08611cfd75
commit a2cb8e8ba2
33 changed files with 3971 additions and 49 deletions

View file

@ -19,8 +19,10 @@ package bind
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math/big"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/external" "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
},
}
}

View file

@ -17,6 +17,7 @@
package external package external
import ( import (
"encoding/json"
"fmt" "fmt"
"math/big" "math/big"
"sync" "sync"
@ -130,6 +131,11 @@ func (api *ExternalSigner) Accounts() []accounts.Account {
func (api *ExternalSigner) Contains(account accounts.Account) bool { func (api *ExternalSigner) Contains(account accounts.Account) bool {
api.cacheMu.RLock() api.cacheMu.RLock()
if api.cache == nil {
api.cacheMu.RUnlock()
api.Accounts()
api.cacheMu.RLock()
}
defer api.cacheMu.RUnlock() defer api.cacheMu.RUnlock()
for _, a := range api.cache { for _, a := range api.cache {
if a.Address == account.Address && (account.URL == (accounts.URL{}) || account.URL == api.URL()) { 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) { func (api *ExternalSigner) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
var res hexutil.Bytes var res hexutil.Bytes
var signAddress = common.NewMixedcaseAddress(account.Address) var signAddress = common.NewMixedcaseAddress(account.Address)
var param = make(map[string]interface{})
if err := json.Unmarshal(data, &param); err != nil {
return nil, err
}
if err := api.client.Call(&res, "account_signData", if err := api.client.Call(&res, "account_signData",
mimeType, mimeType,
&signAddress, // Need to use the pointer here, because of how MarshalJSON is defined &signAddress, // Need to use the pointer here, because of how MarshalJSON is defined
hexutil.Encode(data)); err != nil { param); err != nil {
return nil, err return nil, err
} }
// If V is on 27/28-form, convert to to 0/1 for Clique // If V is on 27/28-form, convert to to 0/1 for Clique

View file

@ -100,6 +100,9 @@ var (
utils.UltraLightServersFlag, utils.UltraLightServersFlag,
utils.UltraLightFractionFlag, utils.UltraLightFractionFlag,
utils.UltraLightOnlyAnnounceFlag, utils.UltraLightOnlyAnnounceFlag,
utils.ServiceChargeFlag,
utils.ServicePaymentFlag,
utils.LightAddressFlag,
utils.WhitelistFlag, utils.WhitelistFlag,
utils.CacheFlag, utils.CacheFlag,
utils.CacheDatabaseFlag, utils.CacheDatabaseFlag,
@ -337,7 +340,7 @@ func startNode(ctx *cli.Context, stack *node.Node) {
if err := stack.Service(&ethService); err != nil { if err := stack.Service(&ethService); err != nil {
utils.Fatalf("Failed to retrieve ethereum service: %v", err) 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 // Set contract backend for les service if local node is
// running as a light client. // running as a light client.
@ -346,7 +349,7 @@ func startNode(ctx *cli.Context, stack *node.Node) {
if err := stack.Service(&lesService); err != nil { if err := stack.Service(&lesService); err != nil {
utils.Fatalf("Failed to retrieve light ethereum service: %v", err) utils.Fatalf("Failed to retrieve light ethereum service: %v", err)
} }
lesService.SetContractBackend(ethClient) lesService.SetBackends(ethClient, ethClient)
} }
go func() { go func() {

View file

@ -94,6 +94,9 @@ var AppHelpFlagGroups = []flagGroup{
utils.UltraLightServersFlag, utils.UltraLightServersFlag,
utils.UltraLightFractionFlag, utils.UltraLightFractionFlag,
utils.UltraLightOnlyAnnounceFlag, utils.UltraLightOnlyAnnounceFlag,
utils.ServiceChargeFlag,
utils.ServicePaymentFlag,
utils.LightAddressFlag,
}, },
}, },
{ {

View file

@ -282,6 +282,19 @@ var (
Name: "ulc.onlyannounce", Name: "ulc.onlyannounce",
Usage: "Ultra light server sends announcements only", 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 // Ethash settings
EthashCacheDirFlag = DirectoryFlag{ EthashCacheDirFlag = DirectoryFlag{
Name: "ethash.cachedir", Name: "ethash.cachedir",
@ -1001,6 +1014,15 @@ func setLes(ctx *cli.Context, cfg *eth.Config) {
if ctx.GlobalIsSet(UltraLightOnlyAnnounceFlag.Name) { if ctx.GlobalIsSet(UltraLightOnlyAnnounceFlag.Name) {
cfg.UltraLightOnlyAnnounce = ctx.GlobalBool(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 // 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) { err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
fullNode, err := eth.New(ctx, cfg) fullNode, err := eth.New(ctx, cfg)
if fullNode != nil && cfg.LightServ > 0 { if fullNode != nil && cfg.LightServ > 0 {
ls, _ := les.NewLesServer(fullNode, cfg) ls, _ := les.NewLesServer(ctx, fullNode, cfg)
fullNode.AddLesServer(ls) fullNode.AddLesServer(ls)
} }
return fullNode, err return fullNode, err

View file

@ -0,0 +1,224 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// package accountbook implements the contract based micropayment for les server.
package accountbook
import (
"context"
"encoding/json"
"errors"
"io"
"math/big"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/contracts/accountbook/contract"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
)
const (
ChallengeTimeWindow = 64 // The default challenge block numbers, which euqals to 16mins
deployTimeout = 5 * time.Minute // The maxmium waiting time for contract deployment.
)
// Cheque is a document that orders a bank(contract) to pay a specific amount
// of money from a person's account to the person in whose name the cheque has
// been issued(contract owner). The cheque is signed by drawer so that he can't
// deny it.
//
// What is different from traditional cheques is: the amount of the cheque is
// cumulative. So that contract can easily check whether the cheque is double-cash
// by the payee.
//
// TODO(rjl493456442) add CHAINID
type Cheque struct {
Drawer common.Address // The drawer of the cheque
ContractAddr common.Address // The address of the accountbook contract(bank address)
Amount *big.Int // The cumulative amount of the issued cheque
Sig [crypto.SignatureLength]byte
}
type chequeRLP struct {
ContractAddr common.Address // The address of the accountbook contract(bank address)
Amount *big.Int // The cumulative amount of the issued cheque
Sig [crypto.SignatureLength]byte
}
// EncodeRLP implements rlp.Encoder, and flattens the necessary fields of a cheque
// into an RLP stream.
func (c *Cheque) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, &chequeRLP{ContractAddr: c.ContractAddr, Amount: c.Amount, Sig: c.Sig})
}
// DecodeRLP implements rlp.Decoder, and loads the rlp-encoded fields of a cheque
// from an RLP stream.
func (c *Cheque) DecodeRLP(s *rlp.Stream) error {
var dec chequeRLP
if err := s.Decode(&dec); err != nil {
return err
}
c.ContractAddr, c.Amount, c.Sig = dec.ContractAddr, dec.Amount, dec.Sig
// If the cheque doesn't contain a signature, skip resolving the drawer address.
if c.Sig == [65]byte{} {
return nil
}
drawer, err := c.recoverDrawer()
if err != nil {
return err
}
c.Drawer = drawer
return nil
}
// recoverDrawer resolves the drawer address from the cheque content
// and signed signature.
func (c *Cheque) recoverDrawer() (common.Address, error) {
// EIP 191 style signatures
//
// Arguments when calculating hash to validate
// 1: byte(0x19) - the initial 0x19 byte
// 2: byte(0) - the version byte (data with intended validator)
// 3: this - the validator address
// -- Application specific data
// 4: amount(uint256) big endian 32bytes
buf := make([]byte, 32)
copy(buf[32-len(c.Amount.Bytes()):], c.Amount.Bytes())
data := append([]byte{0x19, 0x00}, append(c.ContractAddr.Bytes(), buf...)...)
// Transform V from 27/28 to 0/1 according to the yellow paper
c.Sig[64] -= 27
defer func() {
c.Sig[64] += 27
}()
pubkey, err := crypto.SigToPub(crypto.Keccak256(data), c.Sig[:])
if err != nil {
return common.Address{}, err
}
return crypto.PubkeyToAddress(*pubkey), nil
}
// validate verifies whether the cheque is signed properly and all fields
// are filled.
func (c *Cheque) validate(chanAddr common.Address) error {
drawer, err := c.recoverDrawer()
if err != nil {
return err
}
if drawer != c.Drawer {
return errors.New("invalid signature")
}
if c.ContractAddr != chanAddr {
return errors.New("unsolicited cheque")
}
if c.Amount == nil {
return errors.New("incomplete cheque")
}
return nil
}
// sign generates the digital signature for cheque by clef. It's a bit
// different with signWithKey, we need to construct a RPC call with clef
// format.
func (c *Cheque) sign(signFn func(data []byte) ([]byte, error)) error {
// EIP 191 style signatures
//
// Arguments when calculating hash to validate
// 1: byte(0x19) - the initial 0x19 byte
// 2: byte(0) - the version byte (data with intended validator)
// 3: this - the validator address
// -- Application specific data
// 4 : amount(uint256) big endian 32bytes
p := make(map[string]string)
p["address"] = c.ContractAddr.Hex()
buf := make([]byte, 32)
copy(buf[32-len(c.Amount.Bytes()):], c.Amount.Bytes())
p["message"] = hexutil.Encode(buf)
encoded, err := json.Marshal(p)
if err != nil {
return err
}
sig, err := signFn(encoded)
if err != nil {
return err
}
copy(c.Sig[:], sig)
return nil
}
// signWithKey signes the cheque with privatekey. Only use it in testing.
func (c *Cheque) signWithKey(signFn func(digestHash []byte) ([]byte, error)) error {
// EIP 191 style signatures
//
// Arguments when calculating hash to validate
// 1: byte(0x19) - the initial 0x19 byte
// 2: byte(0) - the version byte (data with intended validator)
// 3: this - the validator address
// -- Application specific data
// 4 : amount(uint256) big endian 32bytes
buf := make([]byte, 32)
copy(buf[32-len(c.Amount.Bytes()):], c.Amount.Bytes())
data := append([]byte{0x19, 0x00}, append(c.ContractAddr.Bytes(), buf...)...)
sig, err := signFn(crypto.Keccak256(data))
if err != nil {
return err
}
sig[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
copy(c.Sig[:], sig)
return nil
}
// AccountBook represents a contract instance which holds all drawer's deposits.
type AccountBook struct {
address common.Address
contract *contract.AccountBook
}
// NewAccountBook deploys a new accountbook contract or initializes
// a exist contract by given address.
//
// Note this function can take several minutes for execution.
func newAccountBook(address common.Address, contractBackend bind.ContractBackend) (*AccountBook, error) {
log.Info("Initialized accountbook contract", "address", address)
c, err := contract.NewAccountBook(address, contractBackend)
if err != nil {
return nil, err
}
return &AccountBook{contract: c, address: address}, nil
}
// deployAccountBook deploys the accountbook smart contract and waits the transaction
// is confirmed by network.
func deployAccountBook(auth *bind.TransactOpts, contractBackend bind.ContractBackend, deployBackend bind.DeployBackend) (common.Address, error) {
log.Info("Deploying accountbook contract")
start := time.Now()
_, tx, _, err := contract.DeployAccountBook(auth, contractBackend, uint64(ChallengeTimeWindow))
if err != nil {
return common.Address{}, err
}
context, cancelFn := context.WithTimeout(context.Background(), deployTimeout)
defer cancelFn()
addr, err := bind.WaitDeployed(context, deployBackend, tx)
if err != nil {
return common.Address{}, err
}
log.Info("Deployed accountbook contract", "address", addr, "elapsed", common.PrettyDuration(time.Since(start)))
return addr, nil
}

View file

@ -0,0 +1,171 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accountbook
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
)
var (
// Database schema definitions
// +-----------+
// > Drawer1(client) ---------> | cheque1 |
// Cheque Drawee(Les server) -/ +-----------+
// -/
// +------------+ +------------+ -/ +-----------+
// |server addr1|----->| Contract1 |-/-------> Drawer2(client) ---------> | cheque2 |
// +------------+ +------------+ -\ +-----------+
// -\
// -\ +-----------+
// > Drawer3(client) ---------> | cheque3 |
// +-----------+
// ...
// +------------+ +------------+
// |server addrn|----->| Contractn |
// +------------+ +------------+
//
// Cheque Drawer(Light client)
// +---------------+
// -> Drawee1(server) ---------> | last issued |
// +------------+ -/ +---------------+
// |client addr1|-/---> Drawee2(server) ---------> | last issued |
// +------------+ -\ +---------------+
// -> Drawee3(server) ---------> | last issued |
// +---------------+
// ...
// +------------+
// |client addrn|
// +------------+
contractAddrPrefix = []byte("-a") // contractAddrPrefix + deployer(20bytes) -> contract_addr
chequePrefix = []byte("-c") // chequePrefix + contract_addr(20bytes) + drawer_id(20bytes) -> cheque
issuedPrefix = []byte("-i") // issuedPrefix + drawer_id(20bytes) + contract_addr(20bytes) -> big-endian number
)
// chequeDB keeps all signed cheques issued by customers. It's very important
// to save the cheques properly, otherwise the owner of accountbook can't claim
// the money back.
//
// Cheques are cumulatively confirmed, so only the latest version needs to be stored.
type chequeDB struct {
db ethdb.Database
}
// newChequeDB intiailises the chequedb with given db handler.
func newChequeDB(db ethdb.Database) *chequeDB { return &chequeDB{db: db} }
// readContractAddr returns the contract address deployed by specified deployer.
func (db *chequeDB) readContractAddr(deployer common.Address) *common.Address {
blob, err := db.db.Get(append(contractAddrPrefix, deployer.Bytes()...))
if err != nil {
return nil
}
if len(blob) != common.AddressLength {
return nil
}
addr := common.BytesToAddress(blob)
return &addr
}
// writeContractAddr writes the contract address which deployed by current address.
func (db *chequeDB) writeContractAddr(deployer, contractAddr common.Address) {
if err := db.db.Put(append(contractAddrPrefix, deployer.Bytes()...), contractAddr.Bytes()); err != nil {
log.Crit("Failed to write contract address", "err", err)
}
}
// readCheque returns the last issued cheque for the specified drawer.
// If there is no local_addr => contract_addr mapping, it means we haven't
// deployed the contract yet.
func (db *chequeDB) readCheque(contractAddr, drawer common.Address) *Cheque {
blob, err := db.db.Get(append(append(chequePrefix, contractAddr.Bytes()...), drawer.Bytes()...))
if err != nil {
return nil
}
var cheque Cheque
if err := rlp.DecodeBytes(blob, &cheque); err != nil {
return nil
}
return &cheque
}
// writeCheque writes the last issued cheque from the specific drawer
// into disk.
func (db *chequeDB) writeCheque(contractAddr, drawer common.Address, cheque *Cheque) {
blob, err := rlp.EncodeToBytes(cheque)
if err != nil {
log.Crit("Failed to encode cheque", "error", err)
}
err = db.db.Put(append(append(chequePrefix, contractAddr.Bytes()...), drawer.Bytes()...), blob)
if err != nil {
log.Crit("Failed to store cheque", "error", err)
}
}
// readLastIssued returns the last issued amount by local address to
// specified contract address.
func (db *chequeDB) readLastIssued(drawer, contractAddr common.Address) *big.Int {
blob, err := db.db.Get(append(append(issuedPrefix, drawer.Bytes()...), contractAddr.Bytes()...))
if err != nil {
return nil
}
return new(big.Int).SetBytes(blob)
}
// writeLastIssued writes the last issued amount by local address to
// specified contract address into the disk.
func (db *chequeDB) writeLastIssued(drawer, contractAddr common.Address, amount *big.Int) {
if err := db.db.Put(append(append(issuedPrefix, drawer.Bytes()...), contractAddr.Bytes()...), amount.Bytes()); err != nil {
log.Crit("Failed to store last issue amount", "error", err)
}
}
// allCheques returns all received cheques from different drawers.
func (db *chequeDB) allCheques(contractAddr common.Address) (cheques []*Cheque) {
iter := db.db.NewIteratorWithPrefix(append(chequePrefix, contractAddr.Bytes()...))
defer iter.Release()
for iter.Next() {
var cheque Cheque
if err := rlp.DecodeBytes(iter.Value(), &cheque); err != nil {
continue
}
cheques = append(cheques, &cheque)
}
return
}
// allIssued returns all issued amount from local address to different
// contracts.
func (db *chequeDB) allIssued(drawer common.Address) (addresses []common.Address, amounts []*big.Int) {
iter := db.db.NewIteratorWithPrefix(append(issuedPrefix, drawer.Bytes()...))
defer iter.Release()
for iter.Next() {
var addr common.Address
if len(iter.Key()) != len(issuedPrefix)+common.AddressLength+common.AddressLength {
continue
}
amount := new(big.Int).SetBytes(iter.Value())
copy(addr[:], iter.Key()[len(iter.Key())-common.AddressLength:])
addresses = append(addresses, addr)
amounts = append(amounts, amount)
}
return
}

View file

@ -0,0 +1,182 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accountbook
import (
"math/big"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/crypto"
)
func TestPersistContractAddr(t *testing.T) {
db := newChequeDB(rawdb.NewMemoryDatabase())
local, contract := common.HexToAddress("cafebabe"), common.HexToAddress("deadbeef")
// Read non-existent data
got := db.readContractAddr(local)
if got != nil {
t.Fatalf("Should return nil for non-existent data")
}
db.writeContractAddr(local, contract)
got = db.readContractAddr(local)
if got == nil {
t.Fatalf("Can't read back the written addr")
}
if *got != contract {
t.Fatalf("Mismatch between the written addr with read one, want: %s, got %s", contract.Hex(), got.Hex())
}
}
func TestPersistCheque(t *testing.T) {
db := newChequeDB(rawdb.NewMemoryDatabase())
contract := common.HexToAddress("cafebabe")
key, _ := crypto.GenerateKey()
drawer := crypto.PubkeyToAddress(key.PublicKey)
// Read non-existent data
got := db.readCheque(contract, drawer)
if got != nil {
t.Fatalf("Should return nil for non-existent data")
}
cheque := &Cheque{
Drawer: drawer,
ContractAddr: contract,
Amount: big.NewInt(1),
}
cheque.signWithKey(func(digestHash []byte) ([]byte, error) {
sig, _ := crypto.Sign(digestHash, key)
return sig, nil
})
db.writeCheque(contract, drawer, cheque)
got = db.readCheque(contract, drawer)
if got == nil {
t.Fatalf("Failed to retrieve cheque from db")
}
if !reflect.DeepEqual(cheque, got) {
t.Fatalf("Mismatch between the written cheque with the read one")
}
// Persist a unsigned cheque, it should be retrieved
cheque2 := &Cheque{
Drawer: drawer,
ContractAddr: contract,
Amount: big.NewInt(1),
}
db.writeCheque(contract, drawer, cheque2)
got = db.readCheque(contract, drawer)
if got == nil {
t.Fatalf("Failed to retrieve cheque from db")
}
if cheque2.Amount.Cmp(got.Amount) != 0 || cheque2.ContractAddr != got.ContractAddr {
t.Fatalf("Mismatch between the written cheque with the read one")
}
}
func TestPersistLastIssued(t *testing.T) {
db := newChequeDB(rawdb.NewMemoryDatabase())
drawer, contract := common.HexToAddress("cafebabe"), common.HexToAddress("deadbeef")
amount := big.NewInt(100)
// Read non-existent data
got := db.readLastIssued(drawer, contract)
if got != nil {
t.Fatalf("Should return nil for non-existent data")
}
db.writeLastIssued(drawer, contract, amount)
got = db.readLastIssued(drawer, contract)
if got == nil || got.Cmp(amount) != 0 {
t.Fatalf("Mismatch between the written amount with the read one, want: %d, got: %d", amount, got)
}
}
func TestListCheques(t *testing.T) {
db := newChequeDB(rawdb.NewMemoryDatabase())
contract := common.HexToAddress("cafebabe")
var cheques []*Cheque
for i := 0; i < 10; i++ {
key, _ := crypto.GenerateKey()
drawer := crypto.PubkeyToAddress(key.PublicKey)
cheque := &Cheque{
Drawer: drawer,
ContractAddr: contract,
Amount: big.NewInt(1),
}
cheque.signWithKey(func(digestHash []byte) ([]byte, error) {
sig, _ := crypto.Sign(digestHash, key)
return sig, nil
})
cheques = append(cheques, cheque)
db.writeCheque(contract, drawer, cheque)
}
got := db.allCheques(contract)
if len(got) != len(cheques) {
t.Fatalf("Failed to read all cheques")
}
for _, c1 := range got {
var find bool
for _, c2 := range cheques {
if c1.Drawer == c2.Drawer {
find = true
if !reflect.DeepEqual(c1, c2) {
t.Fatalf("Mismatch between the written cheque with the read one")
}
break
}
}
if !find {
t.Fatalf("Miss cheque in the database")
}
}
// Read non-existent records
got = db.allCheques(common.HexToAddress("deadbeef"))
if len(got) != 0 {
t.Fatalf("Should return nil for non-existent data")
}
}
func TestListAllIssued(t *testing.T) {
db := newChequeDB(rawdb.NewMemoryDatabase())
drawer := common.HexToAddress("cafebabe")
var (
addresses []common.Address
amounts []*big.Int
)
for i := 0; i < 10; i++ {
c, amount := common.BytesToAddress([]byte{byte(i + 1)}), big.NewInt(int64(i+1))
addresses = append(addresses, c)
amounts = append(amounts, amount)
db.writeLastIssued(drawer, c, amount)
}
addresses2, amounts2 := db.allIssued(drawer)
if !reflect.DeepEqual(addresses, addresses2) {
t.Fatalf("Addresses mismatch, want: %v, got: %v", addresses, addresses2)
}
if !reflect.DeepEqual(amounts, amounts2) {
t.Fatalf("Amounts mismatch, want: %v, got: %v", amounts, amounts2)
}
// Read non-existent records
addresses2, amounts2 = db.allIssued(common.HexToAddress("deadbeef"))
if len(addresses2) != 0 || len(amounts2) != 0 {
t.Fatalf("Should return nil for non-existent data")
}
}

View file

@ -0,0 +1,703 @@
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.
package contract
import (
"math/big"
"strings"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
)
// Reference imports to suppress errors if they are not otherwise used.
var (
_ = big.NewInt
_ = strings.NewReader
_ = ethereum.NotFound
_ = abi.U256
_ = bind.Bind
_ = common.Big1
_ = types.BloomLookup
_ = event.NewSubscription
)
// AccountBookABI is the input ABI used to generate the binding from.
const AccountBookABI = "[{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_challengeTimeWindow\",\"type\":\"uint64\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"oldBalance\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newBalance\",\"type\":\"uint256\"}],\"name\":\"balanceChangedEvent\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdrawEvent\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"internalType\":\"address\",\"name\":\"payer\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"sig_v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"sig_r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"sig_s\",\"type\":\"bytes32\"}],\"name\":\"cash\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"challengeTimeWindow\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"claim\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"deposit\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"deposits\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"addresspayable\",\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"paids\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"withdrawRequests\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"createdAt\",\"type\":\"uint128\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"
// AccountBookBin is the compiled bytecode used for deploying new contracts.
var AccountBookBin = "0x608060405234801561001057600080fd5b506040516108ff3803806108ff8339818101604052602081101561003357600080fd5b5051600380546001600160401b039092167401000000000000000000000000000000000000000002600160a01b600160e01b03196001600160a01b03199093163317929092169190911790556108718061008e6000396000f3fe6080604052600436106100865760003560e01c806352df49ec1161005957806352df49ec146101435780638da5cb5b1461019c578063d0e30db0146101cd578063fbf788d6146101d5578063fc7e286d1461022357610086565b80630c18c9ac1461008b5780632e1a7d4d146100d0578063481acb8f146100fc5780634e71d92d1461012e575b600080fd5b34801561009757600080fd5b506100be600480360360208110156100ae57600080fd5b50356001600160a01b0316610256565b60408051918252519081900360200190f35b3480156100dc57600080fd5b506100fa600480360360208110156100f357600080fd5b5035610268565b005b34801561010857600080fd5b50610111610340565b6040805167ffffffffffffffff9092168252519081900360200190f35b34801561013a57600080fd5b506100fa610357565b34801561014f57600080fd5b506101766004803603602081101561016657600080fd5b50356001600160a01b0316610450565b604080516001600160801b03938416815291909216602082015281519081900390910190f35b3480156101a857600080fd5b506101b1610476565b604080516001600160a01b039092168252519081900360200190f35b6100fa610485565b3480156101e157600080fd5b506100fa600480360360a08110156101f857600080fd5b506001600160a01b038135169060208101359060ff604082013516906060810135906080013561051e565b34801561022f57600080fd5b506100be6004803603602081101561024657600080fd5b50356001600160a01b03166107e9565b60006020819052908152604090205481565b806102725761033d565b3360009081526001602052604090205481111561028e5761033d565b336000908152600260205260409020546001600160801b0316156102b15761033d565b6040805180820182526001600160801b0380841682524381166020808401918252336000818152600283528690209451855493518516600160801b029085166001600160801b031990941693909317909316919091179092558251848152925190927f87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e2692908290030190a25b50565b600354600160a01b900467ffffffffffffffff1681565b336000908152600260205260409020546001600160801b03168061037b575061044e565b60035433600090815260026020526040902054600160a01b90910467ffffffffffffffff16600160801b9091046001600160801b0316430310156103bf575061044e565b33600081815260016020908152604080832080546001600160801b0387168082039092556002909352818420849055905191939281156108fc029290818181858888f19350505050158015610418573d6000803e3d6000fd5b50604080518281526001600160801b038416830360208201528151339260008051602061081d833981519152928290030190a250505b565b6002602052600090815260409020546001600160801b0380821691600160801b90041682565b6003546001600160a01b031681565b3360009081526001602052604090205434810181106104d55760405162461bcd60e51b81526004018080602001828103825260218152602001806107fc6021913960400191505060405180910390fd5b33600081815260016020908152604091829020805434908101909155825185815290850191810191909152815160008051602061081d833981519152929181900390910190a250565b6003546001600160a01b0316331461053557600080fd5b6001600160a01b038516600090815260208190526040902054841161055957600080fd5b60408051601960f81b6020808301919091526000602183018190523060601b6022840152603680840189905284518085039091018152605684018086528151918401919091209190526076830180855281905260ff8716609684015260b6830186905260d68301859052925160019260f68082019392601f1981019281900390910190855afa1580156105f0573d6000803e3d6000fd5b505050602060405103516001600160a01b0316866001600160a01b03161461061757600080fd5b6001600160a01b0386166000908152600160209081526040808320549183905282205490919087038083106106d0576001600160a01b03808a1660009081526001602052604080822084870390819055600354915190955092169183156108fc0291849190818181858888f19350505050158015610699573d6000803e3d6000fd5b50604080518481526020810184905281516001600160a01b038c169260008051602061081d833981519152928290030190a2610755565b8215610755576001600160a01b03808a16600090815260016020526040808220829055600354905192169185156108fc0291869190818181858888f19350505050158015610722573d6000803e3d6000fd5b50604080518481526000602082015281516001600160a01b038c169260008051602061081d833981519152928290030190a25b6001600160a01b0389166000908152602081815260408083208b905560029091529020546001600160801b03168210156107de57816107ac576001600160a01b0389166000908152600260205260408120556107de565b6001600160a01b038916600090815260026020526040902080546001600160801b0319166001600160801b0384161790555b505050505050505050565b6001602052600090815260409020548156fe6164646974696f6e206f766572666c6f77206f72207a65726f206465706f7369744decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6a265627a7a723158207177277e626e0106cb630fb83ae0319188ba7f2a583b1f8846d7c25dbb3daa4164736f6c634300050c0032"
// DeployAccountBook deploys a new Ethereum contract, binding an instance of AccountBook to it.
func DeployAccountBook(auth *bind.TransactOpts, backend bind.ContractBackend, _challengeTimeWindow uint64) (common.Address, *types.Transaction, *AccountBook, error) {
parsed, err := abi.JSON(strings.NewReader(AccountBookABI))
if err != nil {
return common.Address{}, nil, nil, err
}
address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(AccountBookBin), backend, _challengeTimeWindow)
if err != nil {
return common.Address{}, nil, nil, err
}
return address, tx, &AccountBook{AccountBookCaller: AccountBookCaller{contract: contract}, AccountBookTransactor: AccountBookTransactor{contract: contract}, AccountBookFilterer: AccountBookFilterer{contract: contract}}, nil
}
// AccountBook is an auto generated Go binding around an Ethereum contract.
type AccountBook struct {
AccountBookCaller // Read-only binding to the contract
AccountBookTransactor // Write-only binding to the contract
AccountBookFilterer // Log filterer for contract events
}
// AccountBookCaller is an auto generated read-only Go binding around an Ethereum contract.
type AccountBookCaller struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// AccountBookTransactor is an auto generated write-only Go binding around an Ethereum contract.
type AccountBookTransactor struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// AccountBookFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
type AccountBookFilterer struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// AccountBookSession is an auto generated Go binding around an Ethereum contract,
// with pre-set call and transact options.
type AccountBookSession struct {
Contract *AccountBook // Generic contract binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// AccountBookCallerSession is an auto generated read-only Go binding around an Ethereum contract,
// with pre-set call options.
type AccountBookCallerSession struct {
Contract *AccountBookCaller // Generic contract caller binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
}
// AccountBookTransactorSession is an auto generated write-only Go binding around an Ethereum contract,
// with pre-set transact options.
type AccountBookTransactorSession struct {
Contract *AccountBookTransactor // Generic contract transactor binding to set the session for
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// AccountBookRaw is an auto generated low-level Go binding around an Ethereum contract.
type AccountBookRaw struct {
Contract *AccountBook // Generic contract binding to access the raw methods on
}
// AccountBookCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
type AccountBookCallerRaw struct {
Contract *AccountBookCaller // Generic read-only contract binding to access the raw methods on
}
// AccountBookTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type AccountBookTransactorRaw struct {
Contract *AccountBookTransactor // Generic write-only contract binding to access the raw methods on
}
// NewAccountBook creates a new instance of AccountBook, bound to a specific deployed contract.
func NewAccountBook(address common.Address, backend bind.ContractBackend) (*AccountBook, error) {
contract, err := bindAccountBook(address, backend, backend, backend)
if err != nil {
return nil, err
}
return &AccountBook{AccountBookCaller: AccountBookCaller{contract: contract}, AccountBookTransactor: AccountBookTransactor{contract: contract}, AccountBookFilterer: AccountBookFilterer{contract: contract}}, nil
}
// NewAccountBookCaller creates a new read-only instance of AccountBook, bound to a specific deployed contract.
func NewAccountBookCaller(address common.Address, caller bind.ContractCaller) (*AccountBookCaller, error) {
contract, err := bindAccountBook(address, caller, nil, nil)
if err != nil {
return nil, err
}
return &AccountBookCaller{contract: contract}, nil
}
// NewAccountBookTransactor creates a new write-only instance of AccountBook, bound to a specific deployed contract.
func NewAccountBookTransactor(address common.Address, transactor bind.ContractTransactor) (*AccountBookTransactor, error) {
contract, err := bindAccountBook(address, nil, transactor, nil)
if err != nil {
return nil, err
}
return &AccountBookTransactor{contract: contract}, nil
}
// NewAccountBookFilterer creates a new log filterer instance of AccountBook, bound to a specific deployed contract.
func NewAccountBookFilterer(address common.Address, filterer bind.ContractFilterer) (*AccountBookFilterer, error) {
contract, err := bindAccountBook(address, nil, nil, filterer)
if err != nil {
return nil, err
}
return &AccountBookFilterer{contract: contract}, nil
}
// bindAccountBook binds a generic wrapper to an already deployed contract.
func bindAccountBook(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) {
parsed, err := abi.JSON(strings.NewReader(AccountBookABI))
if err != nil {
return nil, err
}
return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_AccountBook *AccountBookRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error {
return _AccountBook.Contract.AccountBookCaller.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_AccountBook *AccountBookRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _AccountBook.Contract.AccountBookTransactor.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_AccountBook *AccountBookRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _AccountBook.Contract.AccountBookTransactor.contract.Transact(opts, method, params...)
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_AccountBook *AccountBookCallerRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error {
return _AccountBook.Contract.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_AccountBook *AccountBookTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _AccountBook.Contract.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_AccountBook *AccountBookTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _AccountBook.Contract.contract.Transact(opts, method, params...)
}
// ChallengeTimeWindow is a free data retrieval call binding the contract method 0x481acb8f.
//
// Solidity: function challengeTimeWindow() constant returns(uint64)
func (_AccountBook *AccountBookCaller) ChallengeTimeWindow(opts *bind.CallOpts) (uint64, error) {
var (
ret0 = new(uint64)
)
out := ret0
err := _AccountBook.contract.Call(opts, out, "challengeTimeWindow")
return *ret0, err
}
// ChallengeTimeWindow is a free data retrieval call binding the contract method 0x481acb8f.
//
// Solidity: function challengeTimeWindow() constant returns(uint64)
func (_AccountBook *AccountBookSession) ChallengeTimeWindow() (uint64, error) {
return _AccountBook.Contract.ChallengeTimeWindow(&_AccountBook.CallOpts)
}
// ChallengeTimeWindow is a free data retrieval call binding the contract method 0x481acb8f.
//
// Solidity: function challengeTimeWindow() constant returns(uint64)
func (_AccountBook *AccountBookCallerSession) ChallengeTimeWindow() (uint64, error) {
return _AccountBook.Contract.ChallengeTimeWindow(&_AccountBook.CallOpts)
}
// Deposits is a free data retrieval call binding the contract method 0xfc7e286d.
//
// Solidity: function deposits(address ) constant returns(uint256)
func (_AccountBook *AccountBookCaller) Deposits(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) {
var (
ret0 = new(*big.Int)
)
out := ret0
err := _AccountBook.contract.Call(opts, out, "deposits", arg0)
return *ret0, err
}
// Deposits is a free data retrieval call binding the contract method 0xfc7e286d.
//
// Solidity: function deposits(address ) constant returns(uint256)
func (_AccountBook *AccountBookSession) Deposits(arg0 common.Address) (*big.Int, error) {
return _AccountBook.Contract.Deposits(&_AccountBook.CallOpts, arg0)
}
// Deposits is a free data retrieval call binding the contract method 0xfc7e286d.
//
// Solidity: function deposits(address ) constant returns(uint256)
func (_AccountBook *AccountBookCallerSession) Deposits(arg0 common.Address) (*big.Int, error) {
return _AccountBook.Contract.Deposits(&_AccountBook.CallOpts, arg0)
}
// Owner is a free data retrieval call binding the contract method 0x8da5cb5b.
//
// Solidity: function owner() constant returns(address)
func (_AccountBook *AccountBookCaller) Owner(opts *bind.CallOpts) (common.Address, error) {
var (
ret0 = new(common.Address)
)
out := ret0
err := _AccountBook.contract.Call(opts, out, "owner")
return *ret0, err
}
// Owner is a free data retrieval call binding the contract method 0x8da5cb5b.
//
// Solidity: function owner() constant returns(address)
func (_AccountBook *AccountBookSession) Owner() (common.Address, error) {
return _AccountBook.Contract.Owner(&_AccountBook.CallOpts)
}
// Owner is a free data retrieval call binding the contract method 0x8da5cb5b.
//
// Solidity: function owner() constant returns(address)
func (_AccountBook *AccountBookCallerSession) Owner() (common.Address, error) {
return _AccountBook.Contract.Owner(&_AccountBook.CallOpts)
}
// Paids is a free data retrieval call binding the contract method 0x0c18c9ac.
//
// Solidity: function paids(address ) constant returns(uint256)
func (_AccountBook *AccountBookCaller) Paids(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) {
var (
ret0 = new(*big.Int)
)
out := ret0
err := _AccountBook.contract.Call(opts, out, "paids", arg0)
return *ret0, err
}
// Paids is a free data retrieval call binding the contract method 0x0c18c9ac.
//
// Solidity: function paids(address ) constant returns(uint256)
func (_AccountBook *AccountBookSession) Paids(arg0 common.Address) (*big.Int, error) {
return _AccountBook.Contract.Paids(&_AccountBook.CallOpts, arg0)
}
// Paids is a free data retrieval call binding the contract method 0x0c18c9ac.
//
// Solidity: function paids(address ) constant returns(uint256)
func (_AccountBook *AccountBookCallerSession) Paids(arg0 common.Address) (*big.Int, error) {
return _AccountBook.Contract.Paids(&_AccountBook.CallOpts, arg0)
}
// WithdrawRequests is a free data retrieval call binding the contract method 0x52df49ec.
//
// Solidity: function withdrawRequests(address ) constant returns(uint128 amount, uint128 createdAt)
func (_AccountBook *AccountBookCaller) WithdrawRequests(opts *bind.CallOpts, arg0 common.Address) (struct {
Amount *big.Int
CreatedAt *big.Int
}, error) {
ret := new(struct {
Amount *big.Int
CreatedAt *big.Int
})
out := ret
err := _AccountBook.contract.Call(opts, out, "withdrawRequests", arg0)
return *ret, err
}
// WithdrawRequests is a free data retrieval call binding the contract method 0x52df49ec.
//
// Solidity: function withdrawRequests(address ) constant returns(uint128 amount, uint128 createdAt)
func (_AccountBook *AccountBookSession) WithdrawRequests(arg0 common.Address) (struct {
Amount *big.Int
CreatedAt *big.Int
}, error) {
return _AccountBook.Contract.WithdrawRequests(&_AccountBook.CallOpts, arg0)
}
// WithdrawRequests is a free data retrieval call binding the contract method 0x52df49ec.
//
// Solidity: function withdrawRequests(address ) constant returns(uint128 amount, uint128 createdAt)
func (_AccountBook *AccountBookCallerSession) WithdrawRequests(arg0 common.Address) (struct {
Amount *big.Int
CreatedAt *big.Int
}, error) {
return _AccountBook.Contract.WithdrawRequests(&_AccountBook.CallOpts, arg0)
}
// Cash is a paid mutator transaction binding the contract method 0xfbf788d6.
//
// Solidity: function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) returns()
func (_AccountBook *AccountBookTransactor) Cash(opts *bind.TransactOpts, payer common.Address, amount *big.Int, sig_v uint8, sig_r [32]byte, sig_s [32]byte) (*types.Transaction, error) {
return _AccountBook.contract.Transact(opts, "cash", payer, amount, sig_v, sig_r, sig_s)
}
// Cash is a paid mutator transaction binding the contract method 0xfbf788d6.
//
// Solidity: function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) returns()
func (_AccountBook *AccountBookSession) Cash(payer common.Address, amount *big.Int, sig_v uint8, sig_r [32]byte, sig_s [32]byte) (*types.Transaction, error) {
return _AccountBook.Contract.Cash(&_AccountBook.TransactOpts, payer, amount, sig_v, sig_r, sig_s)
}
// Cash is a paid mutator transaction binding the contract method 0xfbf788d6.
//
// Solidity: function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) returns()
func (_AccountBook *AccountBookTransactorSession) Cash(payer common.Address, amount *big.Int, sig_v uint8, sig_r [32]byte, sig_s [32]byte) (*types.Transaction, error) {
return _AccountBook.Contract.Cash(&_AccountBook.TransactOpts, payer, amount, sig_v, sig_r, sig_s)
}
// Claim is a paid mutator transaction binding the contract method 0x4e71d92d.
//
// Solidity: function claim() returns()
func (_AccountBook *AccountBookTransactor) Claim(opts *bind.TransactOpts) (*types.Transaction, error) {
return _AccountBook.contract.Transact(opts, "claim")
}
// Claim is a paid mutator transaction binding the contract method 0x4e71d92d.
//
// Solidity: function claim() returns()
func (_AccountBook *AccountBookSession) Claim() (*types.Transaction, error) {
return _AccountBook.Contract.Claim(&_AccountBook.TransactOpts)
}
// Claim is a paid mutator transaction binding the contract method 0x4e71d92d.
//
// Solidity: function claim() returns()
func (_AccountBook *AccountBookTransactorSession) Claim() (*types.Transaction, error) {
return _AccountBook.Contract.Claim(&_AccountBook.TransactOpts)
}
// Deposit is a paid mutator transaction binding the contract method 0xd0e30db0.
//
// Solidity: function deposit() returns()
func (_AccountBook *AccountBookTransactor) Deposit(opts *bind.TransactOpts) (*types.Transaction, error) {
return _AccountBook.contract.Transact(opts, "deposit")
}
// Deposit is a paid mutator transaction binding the contract method 0xd0e30db0.
//
// Solidity: function deposit() returns()
func (_AccountBook *AccountBookSession) Deposit() (*types.Transaction, error) {
return _AccountBook.Contract.Deposit(&_AccountBook.TransactOpts)
}
// Deposit is a paid mutator transaction binding the contract method 0xd0e30db0.
//
// Solidity: function deposit() returns()
func (_AccountBook *AccountBookTransactorSession) Deposit() (*types.Transaction, error) {
return _AccountBook.Contract.Deposit(&_AccountBook.TransactOpts)
}
// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d.
//
// Solidity: function withdraw(uint256 amount) returns()
func (_AccountBook *AccountBookTransactor) Withdraw(opts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) {
return _AccountBook.contract.Transact(opts, "withdraw", amount)
}
// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d.
//
// Solidity: function withdraw(uint256 amount) returns()
func (_AccountBook *AccountBookSession) Withdraw(amount *big.Int) (*types.Transaction, error) {
return _AccountBook.Contract.Withdraw(&_AccountBook.TransactOpts, amount)
}
// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d.
//
// Solidity: function withdraw(uint256 amount) returns()
func (_AccountBook *AccountBookTransactorSession) Withdraw(amount *big.Int) (*types.Transaction, error) {
return _AccountBook.Contract.Withdraw(&_AccountBook.TransactOpts, amount)
}
// AccountBookBalanceChangedEventIterator is returned from FilterBalanceChangedEvent and is used to iterate over the raw logs and unpacked data for BalanceChangedEvent events raised by the AccountBook contract.
type AccountBookBalanceChangedEventIterator struct {
Event *AccountBookBalanceChangedEvent // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *AccountBookBalanceChangedEventIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(AccountBookBalanceChangedEvent)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(AccountBookBalanceChangedEvent)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *AccountBookBalanceChangedEventIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *AccountBookBalanceChangedEventIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// AccountBookBalanceChangedEvent represents a BalanceChangedEvent event raised by the AccountBook contract.
type AccountBookBalanceChangedEvent struct {
Addr common.Address
OldBalance *big.Int
NewBalance *big.Int
Raw types.Log // Blockchain specific contextual infos
}
// FilterBalanceChangedEvent is a free log retrieval operation binding the contract event 0x4decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6.
//
// Solidity: event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance)
func (_AccountBook *AccountBookFilterer) FilterBalanceChangedEvent(opts *bind.FilterOpts, addr []common.Address) (*AccountBookBalanceChangedEventIterator, error) {
var addrRule []interface{}
for _, addrItem := range addr {
addrRule = append(addrRule, addrItem)
}
logs, sub, err := _AccountBook.contract.FilterLogs(opts, "balanceChangedEvent", addrRule)
if err != nil {
return nil, err
}
return &AccountBookBalanceChangedEventIterator{contract: _AccountBook.contract, event: "balanceChangedEvent", logs: logs, sub: sub}, nil
}
// WatchBalanceChangedEvent is a free log subscription operation binding the contract event 0x4decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6.
//
// Solidity: event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance)
func (_AccountBook *AccountBookFilterer) WatchBalanceChangedEvent(opts *bind.WatchOpts, sink chan<- *AccountBookBalanceChangedEvent, addr []common.Address) (event.Subscription, error) {
var addrRule []interface{}
for _, addrItem := range addr {
addrRule = append(addrRule, addrItem)
}
logs, sub, err := _AccountBook.contract.WatchLogs(opts, "balanceChangedEvent", addrRule)
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(AccountBookBalanceChangedEvent)
if err := _AccountBook.contract.UnpackLog(event, "balanceChangedEvent", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseBalanceChangedEvent is a log parse operation binding the contract event 0x4decf22c5bd17fc02ce062b1d086abebc7516d810e214d9ad784fc1a023fbba6.
//
// Solidity: event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance)
func (_AccountBook *AccountBookFilterer) ParseBalanceChangedEvent(log types.Log) (*AccountBookBalanceChangedEvent, error) {
event := new(AccountBookBalanceChangedEvent)
if err := _AccountBook.contract.UnpackLog(event, "balanceChangedEvent", log); err != nil {
return nil, err
}
return event, nil
}
// AccountBookWithdrawEventIterator is returned from FilterWithdrawEvent and is used to iterate over the raw logs and unpacked data for WithdrawEvent events raised by the AccountBook contract.
type AccountBookWithdrawEventIterator struct {
Event *AccountBookWithdrawEvent // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *AccountBookWithdrawEventIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(AccountBookWithdrawEvent)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(AccountBookWithdrawEvent)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *AccountBookWithdrawEventIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *AccountBookWithdrawEventIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// AccountBookWithdrawEvent represents a WithdrawEvent event raised by the AccountBook contract.
type AccountBookWithdrawEvent struct {
Addr common.Address
Amount *big.Int
Raw types.Log // Blockchain specific contextual infos
}
// FilterWithdrawEvent is a free log retrieval operation binding the contract event 0x87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e26.
//
// Solidity: event withdrawEvent(address indexed addr, uint256 amount)
func (_AccountBook *AccountBookFilterer) FilterWithdrawEvent(opts *bind.FilterOpts, addr []common.Address) (*AccountBookWithdrawEventIterator, error) {
var addrRule []interface{}
for _, addrItem := range addr {
addrRule = append(addrRule, addrItem)
}
logs, sub, err := _AccountBook.contract.FilterLogs(opts, "withdrawEvent", addrRule)
if err != nil {
return nil, err
}
return &AccountBookWithdrawEventIterator{contract: _AccountBook.contract, event: "withdrawEvent", logs: logs, sub: sub}, nil
}
// WatchWithdrawEvent is a free log subscription operation binding the contract event 0x87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e26.
//
// Solidity: event withdrawEvent(address indexed addr, uint256 amount)
func (_AccountBook *AccountBookFilterer) WatchWithdrawEvent(opts *bind.WatchOpts, sink chan<- *AccountBookWithdrawEvent, addr []common.Address) (event.Subscription, error) {
var addrRule []interface{}
for _, addrItem := range addr {
addrRule = append(addrRule, addrItem)
}
logs, sub, err := _AccountBook.contract.WatchLogs(opts, "withdrawEvent", addrRule)
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(AccountBookWithdrawEvent)
if err := _AccountBook.contract.UnpackLog(event, "withdrawEvent", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseWithdrawEvent is a log parse operation binding the contract event 0x87d5f4772963d1f9b76047158b4ae97c420a1b3bff2a746c828beffd9e7c3e26.
//
// Solidity: event withdrawEvent(address indexed addr, uint256 amount)
func (_AccountBook *AccountBookFilterer) ParseWithdrawEvent(log types.Log) (*AccountBookWithdrawEvent, error) {
event := new(AccountBookWithdrawEvent)
if err := _AccountBook.contract.UnpackLog(event, "withdrawEvent", log); err != nil {
return nil, err
}
return event, nil
}

View file

@ -0,0 +1,212 @@
pragma solidity ^0.5.12;
/**
* @title AccountBook
* @author Gary Rong<garyrong@ethereum.org>
* @dev Implementation of the account book which les server can use to
* @dev process micropayments from customers.
*/
contract AccountBook {
/*
Events
*/
// withdrawEvent is emitted if customer opens a request to withdraw
// deposit.
event withdrawEvent(address indexed addr, uint256 amount);
// balanceChangedEvent is emitted when the deposit balance of customer
// is changed.
event balanceChangedEvent(address indexed addr, uint256 oldBalance, uint256 newBalance);
/*
Definitions
*/
// withdrawRequest defines all necessary fields of
// the withdraw request from customers
struct withdrawRequest {
uint128 amount; // the amount requested to withdraw
uint128 createdAt; // the created block number
}
/*
Modifier
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/*
Public Functions
*/
constructor(uint64 _challengeTimeWindow) public {
owner = msg.sender;
// challengeTimeWindow is decided by owner itself. In theory
// the longer challenge time window, the safer for owner. But
// in the mean time, it will take longer for customers to withdraw
// money. The inconvenience may lead to lose potentional customers.
challengeTimeWindow = _challengeTimeWindow;
}
// deposit adds the amount of money into the account book
function deposit() payable public {
uint256 balance = deposits[msg.sender];
require(balance + msg.value > balance, "addition overflow or zero deposit");
deposits[msg.sender] += msg.value;
emit balanceChangedEvent(msg.sender, balance, balance+msg.value);
}
// withdraw opens a request to withdraw the deposit from the
// account book. Caller has to wait challengeTimeWindow blocks
// time which leaves enough time window for owner to challenge
// the withdraw amount.
// @amount: the amount of deposit to withdraw
function withdraw(uint256 amount) public {
// Ensure it's a meaningful withdraw request
if (amount == 0) {
return;
}
// Ensure the customer has enough deposit to withdraw
if (deposits[msg.sender] < amount) {
return;
}
// Account book can only process one withdraw request
// at the same time.
if (withdrawRequests[msg.sender].amount > 0) {
return;
}
// Convert the amount and block number into uin128 so that
// only 1 slot is necessary(we can save 20,000 gas cost).
withdrawRequests[msg.sender] = withdrawRequest({amount: uint128(amount), createdAt: uint128(block.number)});
emit withdrawEvent(msg.sender, amount);
}
// claim withdraws the deposit from the account book which
// has passed the challenge period.
function claim() public {
uint128 amount = withdrawRequests[msg.sender].amount;
// Short circuit if the withdrawal amount is zero.
// There are several situations can lead to this case:
// * there is no withdrawal request at all
// * there is no withdrawable deposit since all of the deposit
// is used and cashed by owner of the account book
if (amount == 0) {
return;
}
// Ensure the request has passed the challenge period.
if (block.number - withdrawRequests[msg.sender].createdAt < challengeTimeWindow) {
return;
}
// Decrease the balance of customer before transfer.
uint256 balance = deposits[msg.sender];
deposits[msg.sender] -= amount;
delete withdrawRequests[msg.sender]; // Release the withdraw lock.
msg.sender.transfer(amount);
emit balanceChangedEvent(msg.sender, balance, balance-amount);
}
// cash claims the specified amount of money from payer's deposit with
// offchain signature.
//
// For cash operation, since it's called by owner itself, so that it's
// unnecessary to leave a challenge time window.
//
// This function can be called in two cases:
// * the owner of account book thinks there are too many payments made
// by customers
// * if the customer opens a withdraw request which tries to withdraw
// some spent money, it's a way to challenge the request.
//
// @payer: the address of payer who has made a few micropayments off-chain.
// @amount : the amount of money owner wants to cash
// @sig_v : the v-value of the signature
// @sig_r : the r-value of the signature
// @sig_s : the s-value of the signature
function cash(address payer, uint256 amount, uint8 sig_v, bytes32 sig_r, bytes32 sig_s) onlyOwner public {
// In order to prevent the owner to double-cash the
// cheque of customer, we record the cashed amount
// in contract. Only higher signed amount can make
// a valid cash operation.
require(amount > paids[payer]);
// Check the digital signature of the cheque.
//
// EIP 191 style signatures
//
// Arguments when calculating hash to validate
// 1: byte(0x19) - the initial 0x19 byte
// 2: byte(0) - the version byte (data with intended validator)
// 3: this - the validator address
// -- Application specific data
// 4: amount the amount of paid money
// 5: chainID(todo need istanbul fork)
bytes32 hash = keccak256(abi.encodePacked(byte(0x19), byte(0), this, amount));
require(payer == ecrecover(hash, sig_v, sig_r, sig_s));
// Cash all cheques to owner's account directly.
uint256 balance = deposits[payer];
uint256 newBalance;
uint256 diff = amount - paids[payer];
// Move the money into owner's address
if (balance >= diff) {
// Payer has enough deposit to cover all spends
newBalance = balance - diff;
deposits[payer] = newBalance;
owner.transfer(diff);
emit balanceChangedEvent(payer, balance, newBalance);
} else if (balance > 0) {
// It can happen that payer doesn't have enough deposit to cover spends.
// In theory owner should reject all "invalid" cheques off-chain. But if
// some errors occur that owner accept the "useless" cheque, we still support
// owner to cash all "spent" money.
delete deposits[payer];
// Transfer all remaing money into owner's pocket.
owner.transfer(balance);
emit balanceChangedEvent(payer, balance, 0);
}
paids[payer] = amount; // Record all cashed amount in order to prevent "double-cash"
// If customer want to withdraw some spent money, reject
// it by decreaing the amount or just delete the request.
// It's the challenge action for invalid withdraw request.
if (withdrawRequests[payer].amount > newBalance) {
if (newBalance == 0) {
delete withdrawRequests[payer];
} else {
withdrawRequests[payer].amount = uint128(newBalance);
}
}
}
/*
Fields
*/
// paids is the map which contains the cumulative paid amount
// in wei from each customer
mapping(address => uint256) public paids;
// deposits is the map which contains the deposit of customers.
// Customers can withdraw unused deposit back.
mapping(address => uint256) public deposits;
// withdrawRequests is the map which contains all withdraw
// requests from customers, no matter to withdraw all deposit
// or a part.
mapping(address => withdrawRequest) public withdrawRequests;
// owner is the address of the account book owner(the address
// of les server).
address payable public owner;
// challengeTimeWindow is the maximum time that owner can perform
// challenge when customer requests deposit withdrawal. It's count
// by block number.
uint64 public challengeTimeWindow;
}

View file

@ -0,0 +1,465 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package contract
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
)
var (
testChallengeTimeWindow = big.NewInt(4)
)
type Account struct {
key *ecdsa.PrivateKey
addr common.Address
}
type testAccountBook struct {
sim *backends.SimulatedBackend
contract *AccountBook
address common.Address
owner Account
customer Account
}
func newAccount() Account {
key, _ := crypto.GenerateKey()
return Account{addr: crypto.PubkeyToAddress(key.PublicKey), key: key}
}
func newTestContract(t *testing.T) *testAccountBook {
owner, customer := newAccount(), newAccount()
sim := backends.NewSimulatedBackend(core.GenesisAlloc{owner.addr: {Balance: big.NewInt(1000000000)}, customer.addr: {Balance: big.NewInt(1000000000)}}, 10000000)
transactOpts := bind.NewKeyedTransactor(owner.key)
addr, _, c, err := DeployAccountBook(transactOpts, sim, testChallengeTimeWindow.Uint64())
if err != nil {
t.Error("Failed to deploy registrar contract", err)
}
sim.Commit()
return &testAccountBook{
sim: sim,
contract: c,
address: addr,
owner: owner,
customer: customer,
}
}
func (tester *testAccountBook) teardown() {
tester.sim.Close()
}
func (tester *testAccountBook) listenBalanceChangeEvent(res chan bool, callback func(new *big.Int, old *big.Int) bool) {
sink := make(chan *AccountBookBalanceChangedEvent)
sub, err := tester.contract.WatchBalanceChangedEvent(nil, sink, nil)
if err != nil {
res <- false
return
}
defer sub.Unsubscribe()
// Check whether we receive the desired event
select {
case ev := <-sink:
if !callback(ev.NewBalance, ev.OldBalance) {
res <- false
return
}
case <-time.NewTimer(time.Second).C:
res <- false // Timeout
return
}
// Ensure no more additional event receive
select {
case <-sink:
res <- false
return
case <-time.NewTimer(100 * time.Millisecond).C:
res <- true
return
}
}
func (tester *testAccountBook) listenWithdrawEvent(res chan bool, callback func(addr common.Address, amount *big.Int) bool) {
sink := make(chan *AccountBookWithdrawEvent)
sub, err := tester.contract.WatchWithdrawEvent(nil, sink, nil)
if err != nil {
res <- false
return
}
defer sub.Unsubscribe()
// Check whether we receive the desired event
select {
case ev := <-sink:
if !callback(ev.Addr, ev.Amount) {
res <- false
return
}
case <-time.NewTimer(time.Second).C:
res <- false // Timeout
return
}
// Ensure no more additional event receive
select {
case <-sink:
res <- false
return
case <-time.NewTimer(100 * time.Millisecond).C:
res <- true
return
}
}
func (tester *testAccountBook) issueCheque(amount *big.Int) []byte {
buf := make([]byte, 32)
copy(buf[32-len(amount.Bytes()):], amount.Bytes())
data := append([]byte{0x19, 0x00}, append(tester.address.Bytes(), buf...)...)
sig, _ := crypto.Sign(crypto.Keccak256(data), tester.customer.key)
sig[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
return sig
}
func (tester *testAccountBook) commitEmptyBlocks(number int) {
for i := 0; i < number; i++ {
tester.sim.Commit()
}
}
func (tester *testAccountBook) balance(address common.Address) *big.Int {
balance, err := tester.sim.BalanceAt(context.Background(), address, nil)
if err != nil {
return nil
}
return balance
}
func (tester *testAccountBook) contractBalance() *big.Int {
return tester.balance(tester.address)
}
func (tester *testAccountBook) ownerBalance() *big.Int {
return tester.balance(tester.owner.addr)
}
func (tester *testAccountBook) customerBalance() *big.Int {
return tester.balance(tester.customer.addr)
}
func TestDeposit(t *testing.T) {
tester := newTestContract(t)
defer tester.teardown()
eventCh := make(chan bool, 1)
go tester.listenBalanceChangeEvent(eventCh, func(new *big.Int, old *big.Int) bool {
return new.Cmp(big.NewInt(10000)) == 0 && old.Cmp(big.NewInt(0)) == 0
})
// Deposit 10,000 wei
opt := bind.NewKeyedTransactor(tester.customer.key)
opt.Value = big.NewInt(10000)
tester.contract.Deposit(opt)
tester.sim.Commit()
balance, err := tester.contract.Deposits(nil, tester.customer.addr)
if err != nil {
t.Fatalf("Failed to retrieve balanace: %v", err)
}
want := big.NewInt(10000)
if balance.Cmp(want) != 0 {
t.Fatalf("Balance mismtach, want: %d, got: %d", want, balance)
}
if !<-eventCh {
t.Fatalf("Failed for balance change event")
}
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
}
// Deposit 0 wei, we don't accept empty deposit
opt.Value = nil
_, err = tester.contract.Deposit(opt)
if err == nil {
t.Fatalf("Zero deposit should be rejected")
}
}
func TestWithdraw(t *testing.T) {
tester := newTestContract(t)
defer tester.teardown()
// Deposit 10,000 wei
opt := bind.NewKeyedTransactor(tester.customer.key)
opt.Value = big.NewInt(10000)
tester.contract.Deposit(opt)
tester.sim.Commit()
eventCh := make(chan bool, 1)
go tester.listenWithdrawEvent(eventCh, func(addr common.Address, amount *big.Int) bool {
return addr == tester.customer.addr && amount.Cmp(big.NewInt(10000)) == 0
})
// Open the request for withdrawal
opt.Value = nil
tester.contract.Withdraw(opt, big.NewInt(10000)) // Withdraw all money
tester.sim.Commit()
if !<-eventCh {
t.Fatalf("Failed for withdraw event")
}
// Check there is a open withdrawal request
request, err := tester.contract.WithdrawRequests(nil, tester.customer.addr)
if err != nil {
t.Fatalf("Failed to retrieve withdraw request: %v", err)
}
if request.Amount.Cmp(big.NewInt(10000)) != 0 {
t.Fatalf("Withdrawal amount mismatch, want: %d, got: %d", 10000, request.Amount)
}
// Try to claim the deposit before challenge
tester.contract.Claim(opt)
tester.sim.Commit()
balance, err := tester.contract.Deposits(nil, tester.customer.addr)
if err != nil {
t.Fatalf("Failed to retrieve balanace: %v", err)
}
if balance.Cmp(big.NewInt(10000)) != 0 {
t.Fatalf("Deposit can't be withdraw during the challenge period")
}
// Pass the challenge period and withdraw
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
go tester.listenBalanceChangeEvent(eventCh, func(new *big.Int, old *big.Int) bool {
return old.Cmp(big.NewInt(10000)) == 0 && new.Cmp(big.NewInt(0)) == 0
})
tester.contract.Claim(opt)
tester.sim.Commit()
balance, err = tester.contract.Deposits(nil, tester.customer.addr)
if err != nil {
t.Fatalf("Failed to retrieve balanace: %v", err)
}
want := big.NewInt(0)
if balance.Cmp(want) != 0 {
t.Fatalf("Balance mismtach, want: %d, got: %d", want, balance)
}
if !<-eventCh {
t.Fatalf("Failed for balance change event")
}
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
}
}
func TestCash(t *testing.T) {
tester := newTestContract(t)
defer tester.teardown()
// Deposit 10,000 wei
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
customerOpt.Value = big.NewInt(10000)
tester.contract.Deposit(customerOpt)
tester.sim.Commit()
// Customer issues a cheque with amount 1000
sig := tester.issueCheque(big.NewInt(1000))
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
_, err := tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
if err != nil {
t.Fatalf("Failed to cash the signed cheque: %v", err)
}
tester.sim.Commit()
balance, err := tester.contract.Deposits(nil, tester.customer.addr)
if err != nil {
t.Fatalf("Failed to retrieve balanace: %v", err)
}
want := big.NewInt(9000)
if balance.Cmp(want) != 0 {
t.Fatalf("Balance mismtach, want: %d, got: %d", want, balance)
}
// The stored money in contract should also changed
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
}
// Try to double-cash, prevent it.
_, err = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
if err == nil {
t.Fatalf("Double-cash should be prevent")
}
}
func TestChallenge(t *testing.T) {
tester := newTestContract(t)
defer tester.teardown()
// Deposit 10,000 wei
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
customerOpt.Value = big.NewInt(10000)
tester.contract.Deposit(customerOpt)
tester.sim.Commit()
// Customer issues a cheque with amount 1000
sig := tester.issueCheque(big.NewInt(1000))
// Customer tries to withdraw all money including the spent part
customerOpt.Value = nil
_, err := tester.contract.Withdraw(customerOpt, big.NewInt(10000))
if err != nil {
t.Fatalf("Failed to open withdraw request: %v", err)
}
tester.sim.Commit()
// Owner sumbits the evidence to cash the "spent" money
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
_, err = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
if err != nil {
t.Fatalf("Failed to cash the signed cheque: %v", err)
}
tester.sim.Commit()
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
eventCh := make(chan bool, 1)
go tester.listenBalanceChangeEvent(eventCh, func(new *big.Int, old *big.Int) bool {
return old.Cmp(big.NewInt(9000)) == 0 && new.Cmp(big.NewInt(0)) == 0
})
tester.contract.Claim(customerOpt)
tester.sim.Commit()
if !<-eventCh {
t.Fatalf("Failed for balance change event")
}
want := big.NewInt(0)
if balance := tester.contractBalance(); balance == nil || balance.Cmp(want) != 0 {
t.Fatalf("Contract balance mismatch, want: %d, got: %d", want, balance)
}
}
func TestChallengeOutOfWindow(t *testing.T) {
tester := newTestContract(t)
defer tester.teardown()
// Deposit 10,000 wei
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
customerOpt.Value = big.NewInt(10000)
tester.contract.Deposit(customerOpt)
tester.sim.Commit()
// Customer issues a cheque with amount 1000
sig := tester.issueCheque(big.NewInt(1000))
// Customer tries to withdraw all money including the spent part
customerOpt.Value = nil
_, err := tester.contract.Withdraw(customerOpt, big.NewInt(10000))
if err != nil {
t.Fatalf("Failed to open withdraw request: %v", err)
}
tester.sim.Commit()
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
// Now all the money has been claimed.
tester.contract.Claim(customerOpt)
tester.sim.Commit()
// Owner sumbits the evidence to cash the "spent" money, but it's
// too late.
ownerBalanceOld := tester.ownerBalance()
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
ownerOpt.GasPrice = big.NewInt(0)
_, err = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
if err != nil {
t.Fatalf("Failed to cash the signed cheque: %v", err)
}
tester.sim.Commit()
ownerBalanceNew := tester.ownerBalance()
if ownerBalanceNew.Cmp(ownerBalanceOld) != 0 {
t.Fatalf("All claimed money should be cashed")
}
paid, err := tester.contract.Paids(nil, tester.customer.addr)
if err != nil {
t.Fatalf("Failed to retrieve paid amount: %v", err)
}
if paid.Cmp(big.NewInt(1000)) != 0 {
t.Fatal("The paid amount should be set to 1000 even the challenge time window is out")
}
}
func TestGasUsed(t *testing.T) {
tester := newTestContract(t)
defer tester.teardown()
// Deposit 10,000 wei
customerOpt := bind.NewKeyedTransactor(tester.customer.key)
customerOpt.Value = big.NewInt(10000)
tx, _ := tester.contract.Deposit(customerOpt)
tester.sim.Commit()
r, _ := tester.sim.TransactionReceipt(context.Background(), tx.Hash())
fmt.Println("Deposit => gas used:", r.GasUsed)
// Second deposit
customerOpt.Value = big.NewInt(10000)
tx, _ = tester.contract.Deposit(customerOpt)
tester.sim.Commit()
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
fmt.Println("Deposit 2 => gas used:", r.GasUsed)
// Customer issues a cheque with amount 1000
sig := tester.issueCheque(big.NewInt(1000))
// Customer tries to withdraw all money including the spent part
customerOpt.Value = nil
tx, _ = tester.contract.Withdraw(customerOpt, big.NewInt(9000))
tester.sim.Commit()
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
fmt.Println("Withdraw request => gas used:", r.GasUsed)
tester.commitEmptyBlocks(int(testChallengeTimeWindow.Int64()))
// Now all the money has been claimed.
tx, _ = tester.contract.Claim(customerOpt)
tester.sim.Commit()
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
fmt.Println("Deposit claim => gas used:", r.GasUsed)
//
ownerOpt := bind.NewKeyedTransactor(tester.owner.key)
tx, _ = tester.contract.Cash(ownerOpt, tester.customer.addr, big.NewInt(1000), sig[64], common.BytesToHash(sig[:32]), common.BytesToHash(sig[32:64]))
tester.sim.Commit()
r, _ = tester.sim.TransactionReceipt(context.Background(), tx.Hash())
fmt.Println("Cash cheque => gas used:", r.GasUsed)
}

View file

@ -0,0 +1,230 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accountbook
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/contracts/accountbook/contract"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
)
// ChequeDrawee represents the payment drawee in a off-chain payment channel.
type ChequeDrawee struct {
address common.Address
cdb *chequeDB
book *AccountBook
opts *bind.TransactOpts
cBackend bind.ContractBackend
dBackend bind.DeployBackend
}
// NewChequeDrawee creates a payment drawee and deploys the contract if necessary.
func NewChequeDrawee(selfAddr common.Address, opts *bind.TransactOpts, contractBackend bind.ContractBackend, deployBackend bind.DeployBackend, db ethdb.Database) (*ChequeDrawee, error) {
cdb := newChequeDB(db)
var chanAddr common.Address
stored := cdb.readContractAddr(selfAddr)
if stored == nil {
addr, err := deployAccountBook(opts, contractBackend, deployBackend)
if err != nil {
return nil, err
}
cdb.writeContractAddr(selfAddr, addr)
chanAddr = addr
} else {
chanAddr = *stored
}
book, err := newAccountBook(chanAddr, contractBackend)
if err != nil {
return nil, err
}
drawee := &ChequeDrawee{
address: selfAddr,
cdb: cdb,
book: book,
opts: opts,
cBackend: contractBackend,
dBackend: deployBackend,
}
return drawee, nil
}
// ContractAddr returns the address of deployed accountbook contract.
func (drawee *ChequeDrawee) ContractAddr() common.Address {
return drawee.book.address
}
// AddCheque receives a cheque from drawer, check the validity and store
// it locally.
//
// In the mean time, this function will return the cumulative uncash amount
// for auto cash triggering.
func (drawee *ChequeDrawee) AddCheque(c *Cheque) (*big.Int, *big.Int, error) {
// Ensure the cheque is signed properly
if err := c.validate(drawee.ContractAddr()); err != nil {
return nil, nil, err
}
// Ensure the drawer has enough balance to cover the expense.
unspent, err := drawee.Unspent(c.Drawer)
if err != nil {
return nil, nil, err
}
// Figure out the net amount of this cheque.
lastReceived := drawee.cdb.readCheque(drawee.book.address, c.Drawer)
var net *big.Int
if lastReceived == nil {
net = c.Amount
} else if lastReceived.Amount.Cmp(c.Amount) >= 0 {
// There are many cases can lead to this situation:
// * Drawer passes a stale cheque deliberately
// * Drawer's chequedb is broken, it loses all payment history
// In order to help drawer to recover the payment history,
// return an evidence here.
return nil, nil, &StaleChequeError{Msg: "stale cheque", Evidence: lastReceived}
} else {
net = new(big.Int).Sub(c.Amount, lastReceived.Amount)
}
if unspent.Cmp(net) < 0 {
return nil, nil, ErrNotEnoughDeposit
}
// Calculate uncashed amount from this drawer.
paid, err := drawee.book.contract.Paids(nil, c.Drawer)
if err != nil {
return nil, nil, err
}
// Pass the validation, save it into disk.
drawee.cdb.writeCheque(drawee.book.address, c.Drawer, c)
return net, new(big.Int).Sub(c.Amount, paid), nil
}
// +------------- Deposit -----------+
// | |
// +-----------+-----------+------------+
// | Spent | Unspent | Withdrawed |
// +-----------+-----------+-----------+------------+
// | Paid | Unpaid |
// +-----------+-----------+
// | |
// +--- Total Issued ---+
// Unpaid returns unpaid amount of the specified drawer in the channel.
// The calculation method is using total_received(total_issued) minus cashed(paid).
// However the total_received record can be missing due to db corrupt. If this happen,
// we can only adjust the total_received to paid amount and drawer can double-spend
// the uncashed part.
// If the returned error is nil, the returned value should always be non-nil.
func (drawee *ChequeDrawee) Unpaid(addr common.Address) (*big.Int, error) {
// Check how much we have already cashed
paid, err := drawee.book.contract.Paids(nil, addr)
if err != nil {
return nil, err
}
lastReceived := drawee.cdb.readCheque(drawee.book.address, addr)
// We never receive the cheque from this address or local db is corrupt
if lastReceived == nil {
// We have cashing record in contract, but total_received is missing,
// db is corrupt.
if paid.Uint64() != 0 {
// Write a cheque without signature and drawer, we only need the issued amount.
drawee.cdb.writeCheque(drawee.book.address, addr, &Cheque{Amount: paid, ContractAddr: drawee.ContractAddr()})
}
return big.NewInt(0), nil
}
// We have cashing record in contract, but total_received is lower than
// the cashed amount, db is corrupt.
if lastReceived.Amount.Cmp(paid) < 0 {
// Write a cheque without signature and drawer, we only need the issued amount.
drawee.cdb.writeCheque(drawee.book.address, addr, &Cheque{Amount: paid, ContractAddr: drawee.ContractAddr()})
return big.NewInt(0), nil
}
return new(big.Int).Sub(lastReceived.Amount, paid), nil
}
// Unspent returns all unspent balance of the specified drawer in the channel.
// According to the diagram of balance, we can see unspent part is: deposit-withdrawed-unpaid.
// If the returned error is nil, the returned balance should always be non-nil.
func (drawee *ChequeDrawee) Unspent(addr common.Address) (*big.Int, error) {
// Fetch deposit balance from the contract.
balance, err := drawee.book.contract.Deposits(nil, addr)
if err != nil {
return nil, err
}
unpaid, err := drawee.Unpaid(addr)
if err != nil {
return nil, err
}
// If no spendable balance or even worse the drawer already spends the
// money exceeds all deposit.
if unpaid.Cmp(balance) > 0 {
return nil, ErrNotEnoughDeposit
}
remaining := new(big.Int).Sub(balance, unpaid)
req, err := drawee.book.contract.WithdrawRequests(nil, addr)
if err != nil {
return nil, err
}
// Drawer has a opened withdrawal request, no matter it passes the challenge
// period or not, minus this part.
if remaining.Cmp(req.Amount) < 0 {
return nil, ErrNotEnoughDeposit
}
return new(big.Int).Sub(remaining, req.Amount), nil
}
// Cash cashes all unpaid payment by given drawer.
func (drawee *ChequeDrawee) Cash(context context.Context, drawer common.Address, sync bool) error {
unpaid, err := drawee.Unpaid(drawer)
if err != nil {
return err
}
if unpaid.Uint64() == 0 {
return nil // Nothing to cash
}
lastReceived := drawee.cdb.readCheque(drawee.book.address, drawer) // Can't be nil here.
tx, err := drawee.book.contract.Cash(drawee.opts, drawer, lastReceived.Amount, lastReceived.Sig[64], common.BytesToHash(lastReceived.Sig[:32]), common.BytesToHash(lastReceived.Sig[32:64]))
if err != nil {
return err
}
if sync {
_, err := bind.WaitMined(context, drawee.dBackend, tx)
if err != nil {
return err
}
}
log.Info("Cashed cheque", "drawer", drawer, "amount", unpaid, "cumulative", lastReceived.Amount)
return nil
}
// ListenWithdraw watches new withdraw event triggered by drawers.
func (drawee *ChequeDrawee) ListenWithdraw() (event.Subscription, chan *contract.AccountBookWithdrawEvent, error) {
sink := make(chan *contract.AccountBookWithdrawEvent)
sub, err := drawee.book.contract.WatchWithdrawEvent(nil, sink, nil)
if err != nil {
return nil, nil, err
}
return sub, sink, nil
}
// ListCheques returns all cheques drawee received.
func (drawee *ChequeDrawee) ListCheques() []*Cheque {
return drawee.cdb.allCheques(drawee.ContractAddr())
}

View file

@ -0,0 +1,259 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accountbook
import (
"context"
"crypto/ecdsa"
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
)
type testEnv struct {
db ethdb.Database
draweeKey *ecdsa.PrivateKey
draweeAddr common.Address
drawerKey *ecdsa.PrivateKey
drawerAddr common.Address
backend *backends.SimulatedBackend
}
func newTestEnv(t *testing.T) *testEnv {
db := rawdb.NewMemoryDatabase()
key, _ := crypto.GenerateKey()
key2, _ := crypto.GenerateKey()
addr := crypto.PubkeyToAddress(key.PublicKey)
addr2 := crypto.PubkeyToAddress(key2.PublicKey)
sim := backends.NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(1000000000)}, addr2: {Balance: big.NewInt(1000000000)}}, 10000000)
return &testEnv{
db: db,
draweeKey: key,
draweeAddr: addr,
drawerKey: key2,
drawerAddr: addr2,
backend: sim,
}
}
func (env *testEnv) close() { env.backend.Close() }
func (env *testEnv) issueCheque(amount *big.Int, chanAddr common.Address) *Cheque {
cheque := &Cheque{
Drawer: env.drawerAddr,
ContractAddr: chanAddr,
Amount: amount,
}
cheque.signWithKey(func(digestHash []byte) ([]byte, error) {
sig, _ := crypto.Sign(digestHash, env.drawerKey)
return sig, nil
})
return cheque
}
func (env *testEnv) spendAndCheck(t *testing.T, drawee *ChequeDrawee, amount *big.Int, expectErr error, expectNet *big.Int, expectUnpaid *big.Int, expectUnspent *big.Int) {
cheque := env.issueCheque(amount, drawee.ContractAddr())
net, unpaid, err := drawee.AddCheque(cheque)
if expectErr != nil {
if err.Error() != expectErr.Error() {
t.Fatalf("Error mismatch, want: %v, got: %v", expectErr, err)
}
return
}
if net.Cmp(expectNet) != 0 {
t.Fatalf("Net amount mismatch, want: %v, got: %v", expectNet, net)
}
if unpaid.Cmp(expectUnpaid) != 0 {
t.Fatalf("Unpaid amount mismatch, want: %v, got: %v", expectUnpaid, unpaid)
}
unspent, _ := drawee.Unspent(env.drawerAddr)
if unspent.Cmp(expectUnspent) != 0 {
t.Fatalf("Unspent amount mismatch, want: %v, got: %v", expectUnspent, unspent)
}
}
func TestDeployment(t *testing.T) {
env := newTestEnv(t)
defer env.close()
var exit = make(chan struct{})
defer close(exit)
// Start the automatic blockchain.
go func() {
ticker := time.NewTicker(time.Millisecond * 100)
for {
select {
case <-ticker.C:
env.backend.Commit()
case <-exit:
return
}
}
}()
// Deploy the contract if missing
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
if err != nil {
t.Fatalf("Faield to deploy contract, err: %v", err)
}
addr := drawee.cdb.readContractAddr(env.draweeAddr)
if addr == nil {
t.Fatalf("Failed to deploy contract")
}
if *addr != drawee.book.address {
t.Fatalf("Contract address mismatch, want: %v, got: %v", drawee.book.address, *addr)
}
// Restart, no deploy needed
drawee, _ = NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
addr2 := drawee.cdb.readContractAddr(env.draweeAddr)
if addr2 == nil {
t.Fatalf("Failed to reload contract")
}
if *addr2 != *addr {
t.Fatalf("Contract address mismatch, want: %v, got: %v", *addr, *addr2)
}
// Remove the db explicitly
newdb := rawdb.NewMemoryDatabase()
drawee, _ = NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, newdb)
addr3 := drawee.cdb.readContractAddr(env.draweeAddr)
if addr3 == nil {
t.Fatalf("Failed to re-deploy contract")
}
if *addr3 == *addr {
t.Fatalf("New contract address expected")
}
// Drawee changes the key, new contract should be deployed
drawee, _ = NewChequeDrawee(env.drawerAddr, bind.NewKeyedTransactor(env.drawerKey), env.backend, env.backend, newdb)
addr4 := drawee.cdb.readContractAddr(env.drawerAddr)
if addr4 == nil {
t.Fatalf("Failed to re-deploy contract")
}
if *addr4 == *addr3 {
t.Fatalf("New contract address expected")
}
}
func TestAddCheque(t *testing.T) {
env := newTestEnv(t)
defer env.close()
var exit = make(chan struct{})
defer close(exit)
// Start the automatic blockchain.
go func() {
ticker := time.NewTicker(time.Millisecond * 100)
for {
select {
case <-ticker.C:
env.backend.Commit()
case <-exit:
return
}
}
}()
// Deploy the contract if missing
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
if err != nil {
t.Fatalf("Faield to deploy contract, err: %v", err)
}
// Ensure we can reject all cheques which doesn't enough fund backup
env.spendAndCheck(t, drawee, big.NewInt(100), ErrNotEnoughDeposit, nil, nil, nil)
// Deposit enough money for drawer
opt := bind.NewKeyedTransactor(env.drawerKey)
opt.Value = big.NewInt(200)
tx, _ := drawee.book.contract.Deposit(opt)
bind.WaitMined(context.Background(), env.backend, tx)
unspent, _ := drawee.Unspent(env.drawerAddr)
if unspent.Uint64() != 200 {
t.Fatalf("Balance mismatch")
}
// Spend 100, ensure it's successful
env.spendAndCheck(t, drawee, big.NewInt(100), nil, big.NewInt(100), big.NewInt(100), big.NewInt(100))
// Spend another 100, ensure it's also successful
env.spendAndCheck(t, drawee, big.NewInt(200), nil, big.NewInt(100), big.NewInt(200), big.NewInt(0))
// Remove the cheque db explicitly
drawee.cdb = newChequeDB(rawdb.NewMemoryDatabase())
// Drawer can double-spend the part which we haven't cashed.
env.spendAndCheck(t, drawee, big.NewInt(100), nil, big.NewInt(100), big.NewInt(100), big.NewInt(100))
env.spendAndCheck(t, drawee, big.NewInt(150), nil, big.NewInt(50), big.NewInt(150), big.NewInt(50))
// Cash all received payments
drawee.Cash(context.Background(), env.drawerAddr, true)
// Remove the cheque db explicitly again
drawee.cdb = newChequeDB(rawdb.NewMemoryDatabase())
// We can repair the broken db and reject stale cheque
env.spendAndCheck(t, drawee, big.NewInt(100), &StaleChequeError{Msg: "stale cheque"}, nil, nil, nil)
env.spendAndCheck(t, drawee, big.NewInt(150), &StaleChequeError{Msg: "stale cheque"}, nil, nil, nil)
env.spendAndCheck(t, drawee, big.NewInt(200), nil, big.NewInt(50), big.NewInt(50), big.NewInt(0))
env.spendAndCheck(t, drawee, big.NewInt(250), ErrNotEnoughDeposit, nil, nil, nil)
}
// This function tests a special scenario:
// drawer deposits some money and then opens the withdrawal request quickly.
// Then it wants to spend some withdrawed money.
func TestWithdrawInAdvance(t *testing.T) {
env := newTestEnv(t)
defer env.close()
var exit = make(chan struct{})
defer close(exit)
// Start the automatic blockchain.
go func() {
ticker := time.NewTicker(time.Millisecond * 100)
for {
select {
case <-ticker.C:
env.backend.Commit()
case <-exit:
return
}
}
}()
// Deploy the contract if missing
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
if err != nil {
t.Fatalf("Faield to deploy contract, err: %v", err)
}
// Deposit enough money for drawer
opt := bind.NewKeyedTransactor(env.drawerKey)
opt.Value = big.NewInt(200)
tx, _ := drawee.book.contract.Deposit(opt)
bind.WaitMined(context.Background(), env.backend, tx)
// Open withdrawal request immediately
opt = bind.NewKeyedTransactor(env.drawerKey)
tx, _ = drawee.book.contract.Withdraw(opt, big.NewInt(150))
bind.WaitMined(context.Background(), env.backend, tx)
// Ensure all withdrawed part can't be double spend
env.spendAndCheck(t, drawee, big.NewInt(100), ErrNotEnoughDeposit, nil, nil, nil)
env.spendAndCheck(t, drawee, big.NewInt(50), nil, big.NewInt(50), big.NewInt(50), big.NewInt(0))
}

View file

@ -0,0 +1,314 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accountbook
import (
"context"
"errors"
"math/big"
"sync/atomic"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
// ChequeDrawer represents the payment drawer in a off-chain payment channel.
type ChequeDrawer struct {
withdrawRequest uint64
cdb *chequeDB
contractBackend bind.ContractBackend
deployBackend bind.DeployBackend
book *AccountBook
selfAddr common.Address
txSigner *bind.TransactOpts
keySigner func(data []byte) ([]byte, error) // Used for testing, cheque signer
chequeSigner func(data []byte) ([]byte, error) // Used for production environment, cheque signer
}
func NewChequeDrawer(txSigner *bind.TransactOpts, chequeSigner func(data []byte) ([]byte, error), selfAddr common.Address, contractAddr common.Address, contractBackend bind.ContractBackend, deployBackend bind.DeployBackend, db ethdb.Database) (*ChequeDrawer, error) {
if contractAddr == (common.Address{}) {
return nil, errors.New("empty contract address")
}
book, err := newAccountBook(contractAddr, contractBackend)
if err != nil {
return nil, err
}
drawer := &ChequeDrawer{
cdb: newChequeDB(db),
contractBackend: contractBackend,
deployBackend: deployBackend,
selfAddr: selfAddr,
book: book,
txSigner: txSigner,
chequeSigner: chequeSigner,
}
return drawer, nil
}
// ContractAddr returns the address of deployed accountbook contract.
func (drawer *ChequeDrawer) ContractAddr() common.Address {
return drawer.book.address
}
// Deposit transfers the given amount wei into the contract. If the sync is true,
// this function will wait until the transaction is included and return the actual
// transaction status.
func (drawer *ChequeDrawer) Deposit(context context.Context, amount *big.Int) (bool, error) {
// Create an independent auth opt
depositOpt := &bind.TransactOpts{
From: drawer.selfAddr,
Signer: drawer.txSigner.Signer,
Value: amount,
}
tx, err := drawer.book.contract.Deposit(depositOpt)
if err != nil {
return false, err
}
receipt, err := bind.WaitMined(context, drawer.deployBackend, tx)
if err != nil {
return false, err
}
return receipt.Status == types.ReceiptStatusSuccessful, nil
}
// +------------- Deposit ------------+
// | |
// +-----------+-----------+------------+
// | Spent | Unspent | Withdrawed |
// +-----------+-----------+-----------+------------+
// | Paid | Unpaid |
// +-----------+-----------+
// | |
// +--- Total Issued ---+
// Unpaid returns unpaid amount in the channel. The calculation method is using
// total_issued minus paid. However the total_issued record can be missing due
// to db corrupt. If this happen, we can reset the total issued amount by repair.
// If the returned error is nil, the returned value should always be non-nil.
//
// todo(rjl493456442) it's too expensive for light client to retrieve paid amount
// every time.
func (drawer *ChequeDrawer) Unpaid() (*big.Int, error) {
// Check how much we have already cashed
paid, err := drawer.book.contract.Paids(nil, drawer.selfAddr)
if err != nil {
return nil, err
}
lastIssued := drawer.cdb.readLastIssued(drawer.selfAddr, drawer.book.address)
// We never issue any cheque from this address or local db is corrupt
if lastIssued == nil {
// Drawee has already cashed a few cheques, local db must be corrupt,
// repair it. But we still have no clue how much we have issued which
// is not cashed by drawee yet.
if paid.Uint64() != 0 {
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, paid)
}
return big.NewInt(0), nil
}
// Drawee has already cashed a few cheques, but total_issued is lower than
// the cashed amount, db is corrupt. But we still have no clue how much we
// have issued which is not cashed by drawee yet.
if lastIssued.Cmp(paid) < 0 {
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, paid)
return big.NewInt(0), nil
}
return new(big.Int).Sub(lastIssued, paid), nil
}
// Unspent returns all unspent balance of ourselves in the channel. It can
// happen that we get a larger value then real unspent amount due to the
// data loss.
// If the returned error is nil, the returned balance should always be non-nil.
func (drawer *ChequeDrawer) Unspent() (*big.Int, error) {
// Fetch deposit balance from the contract.
balance, err := drawer.book.contract.Deposits(nil, drawer.selfAddr)
if err != nil {
return nil, err
}
unpaid, err := drawer.Unpaid()
if err != nil {
return nil, err
}
// If no spendable balance or even worse we already spend the
// money exceeds all deposit.
if unpaid.Cmp(balance) > 0 {
return big.NewInt(0), ErrNotEnoughDeposit
}
remaining := new(big.Int).Sub(balance, unpaid)
req, err := drawer.book.contract.WithdrawRequests(nil, drawer.selfAddr)
if err != nil {
return nil, err
}
// Drawer has a opened withdrawal request, no matter it passes the challenge
// period or not, minus this part.
if req.Amount.Uint64() != 0 && remaining.Cmp(req.Amount) < 0 {
return big.NewInt(0), ErrNotEnoughDeposit
}
return new(big.Int).Sub(remaining, req.Amount), nil
}
// IssueCheque creates a cheque for issuing specified amount money for payee.
//
// Whenever the drawer creates a cheque and sends it to drawee, drawee has
// the permission to cash the deposit of drawer in the contract.
//
// Because of the possible data loss, we can issue some double-spend cheques,
// they will be rejected by drawee.
//
// In the mean time, this function will also return the remaining unspent
// to trigger auto deposit.
func (drawer *ChequeDrawer) IssueCheque(amount *big.Int) (*Cheque, *big.Int, error) {
if amount == nil || amount.Uint64() == 0 {
return nil, nil, errors.New("invalid issue amount")
}
unspent, err := drawer.Unspent()
if err != nil {
return nil, nil, err
}
if unspent.Cmp(amount) < 0 {
return nil, nil, ErrNotEnoughDeposit
}
var newAmount *big.Int
// If local chequedb is broken, the new amount maybe is a invalid stale number.
// Finally drawee will show us the evidence which we signed before, we can repair
// broken db.
lastIssued := drawer.cdb.readLastIssued(drawer.selfAddr, drawer.book.address)
if lastIssued == nil {
newAmount = amount
} else {
newAmount = new(big.Int).Add(lastIssued, amount)
}
// Assmeble the cheque and sign it.
cheque := &Cheque{
Drawer: drawer.selfAddr,
Amount: newAmount,
ContractAddr: drawer.book.address,
}
// Uses keySigner if we are testing.
if drawer.keySigner != nil {
if err := cheque.signWithKey(drawer.keySigner); err != nil {
return nil, nil, err
}
} else {
if err := cheque.sign(drawer.chequeSigner); err != nil {
return nil, nil, err
}
}
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, newAmount)
return cheque, new(big.Int).Sub(unspent, amount), nil
}
// Withdraw submits a on-chain transaction to open the withdrawal request to
// withdraw all deposit.
func (drawer *ChequeDrawer) Withdraw(context context.Context) error {
// In the channel contract, we only allow one withdrawal operation
// at the same time. Ensure there is no opened withdrawl request.
req, err := drawer.book.contract.WithdrawRequests(nil, drawer.selfAddr)
if err != nil {
return err
}
if req.Amount.Uint64() != 0 {
atomic.StoreUint64(&drawer.withdrawRequest, req.CreatedAt.Uint64())
log.Info("Ongoing withdrawal operation", "amount", req.Amount, "createAt", req.CreatedAt)
return errors.New("duplicate withdraw operation")
}
unspent, err := drawer.Unspent()
if err != nil {
return err
}
if unspent.Uint64() == 0 {
return errors.New("no withdrawable balance")
}
// todo(rjl493456442) add a threshold checking, ignore small balance.
tx, err := drawer.book.contract.Withdraw(drawer.txSigner, unspent)
if err != nil {
return err
}
receipt, err := bind.WaitMined(context, drawer.deployBackend, tx)
if err != nil {
return err
}
if receipt.Status == types.ReceiptStatusSuccessful {
ret, err := drawer.book.contract.WithdrawRequests(nil, drawer.selfAddr)
if err != nil {
return err
}
atomic.StoreUint64(&drawer.withdrawRequest, ret.CreatedAt.Uint64())
}
return nil
}
// WithdrawalRecord returns the create block number of ongoing withdrawal request.
func (drawer *ChequeDrawer) WithdrawalRecord() uint64 {
return atomic.LoadUint64(&drawer.withdrawRequest)
}
// ResetWithdrawlRecord resets withdrawal record when we submit the cash transaction.
func (drawer *ChequeDrawer) ResetWithdrawlRecord() {
atomic.StoreUint64(&drawer.withdrawRequest, 0)
}
// Claim submits a on-chain transaction to claim all claimable balance.
func (drawer *ChequeDrawer) Claim(context context.Context) (bool, error) {
tx, err := drawer.book.contract.Claim(drawer.txSigner)
if err != nil {
return false, err
}
receipt, err := bind.WaitMined(context, drawer.deployBackend, tx)
if err != nil {
return false, err
}
return receipt.Status == types.ReceiptStatusSuccessful, nil
}
// CodeHash returns the code hash of payment channel.
func (drawer *ChequeDrawer) CodeHash(context context.Context) (common.Hash, error) {
code, err := drawer.deployBackend.CodeAt(context, drawer.book.address, nil)
if err != nil {
return common.Hash{}, err
}
return crypto.Keccak256Hash(code), nil
}
// Amend amends the local cheque db with externally provided cheque which is issued
// by ourselves.
func (drawer *ChequeDrawer) Amend(cheque *Cheque) error {
if err := cheque.validate(drawer.book.address); err != nil {
return err
}
if cheque.Drawer != drawer.selfAddr {
return errors.New("invalid evidence")
}
// If local chequedb is corrupt, we can lose some payment records.
// Since the amount of cheque is cumulative, so we need the evidence
// from drawee to amend the local db.
lastIssued := drawer.cdb.readLastIssued(drawer.selfAddr, drawer.book.address)
if lastIssued == nil || lastIssued.Cmp(cheque.Amount) < 0 {
drawer.cdb.writeLastIssued(drawer.selfAddr, drawer.book.address, cheque.Amount)
}
return nil
}
// Payed returns the total payed amount in this channel.
func (drawer *ChequeDrawer) Payed() *big.Int {
return drawer.cdb.readLastIssued(drawer.selfAddr, drawer.ContractAddr())
}

View file

@ -0,0 +1,174 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accountbook
import (
"context"
"errors"
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/crypto"
)
func (env *testEnv) commitEmptyBlocks(n int) {
for i := 0; i < n; i++ {
env.backend.Commit()
}
}
func (env *testEnv) issueAndCheck(t *testing.T, drawer *ChequeDrawer, amount *big.Int, expectErr error, expectCum *big.Int, expectUnspent *big.Int) *Cheque {
cheque, unspent, err := drawer.IssueCheque(amount)
if expectErr != nil {
if err.Error() != expectErr.Error() {
t.Fatalf("Error mismatch, want: %v, got: %v", expectErr, err)
}
return nil
}
if unspent.Cmp(expectUnspent) != 0 {
t.Fatalf("Unspent amount mismatch, want: %v, got: %v", expectUnspent, unspent)
}
if cheque.Amount.Cmp(expectCum) != 0 {
t.Fatalf("Cumulative spent amount mismatch, want: %v, got: %v", expectCum, cheque.Amount)
}
return cheque
}
func TestIssueCheque(t *testing.T) {
env := newTestEnv(t)
defer env.close()
var exit = make(chan struct{})
defer close(exit)
// Start the automatic blockchain.
go func() {
ticker := time.NewTicker(time.Millisecond * 100)
for {
select {
case <-ticker.C:
env.backend.Commit()
case <-exit:
return
}
}
}()
// Deploy the contract if missing
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
if err != nil {
t.Fatalf("Faield to deploy contract, err: %v", err)
}
drawer, err := NewChequeDrawer(bind.NewKeyedTransactor(env.drawerKey), nil, env.drawerAddr, drawee.ContractAddr(), env.backend, env.backend, env.db)
if err != nil {
t.Fatalf("Faield to create drawer, err: %v", err)
}
drawer.keySigner = func(data []byte) ([]byte, error) {
sig, _ := crypto.Sign(data, env.drawerKey)
return sig, nil
}
// Reject all invalid issue operation
env.issueAndCheck(t, drawer, big.NewInt(0), errors.New("invalid issue amount"), nil, nil)
env.issueAndCheck(t, drawer, big.NewInt(100), ErrNotEnoughDeposit, nil, nil)
// Deposit some funds into the contract
drawer.Deposit(context.Background(), big.NewInt(200))
env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(50), big.NewInt(150))
lastIssued := env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(100), big.NewInt(100))
// Cash all payments
tx, err := drawer.book.contract.Cash(bind.NewKeyedTransactor(env.draweeKey), env.drawerAddr, lastIssued.Amount, lastIssued.Sig[64], common.BytesToHash(lastIssued.Sig[:32]), common.BytesToHash(lastIssued.Sig[32:64]))
if err != nil {
t.Fatalf("Failed to cash payment, err: %v", err)
}
bind.WaitMined(context.Background(), env.backend, tx)
// Remove chequedb explictly
drawer.cdb = newChequeDB(rawdb.NewMemoryDatabase())
// Ensure we can repair the broken db!
env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(150), big.NewInt(50))
lastIssued = env.issueAndCheck(t, drawer, big.NewInt(50), nil, big.NewInt(200), big.NewInt(0))
env.issueAndCheck(t, drawer, big.NewInt(50), ErrNotEnoughDeposit, nil, nil)
// Remove chequedb explictly again!
drawer.cdb = newChequeDB(rawdb.NewMemoryDatabase())
// Amend the broken chequedb
if err := drawer.Amend(lastIssued); err != nil {
t.Fatalf("Failed to amend broken chequedb, err: %v", err)
}
unspent, err := drawer.Unspent()
if err != nil {
t.Fatalf("Failed to retrieve unspent part, err: %v", err)
}
if unspent.Uint64() != 0 {
t.Fatalf("Failed to ament the chequedb")
}
}
func TestWithdrawAndClaim(t *testing.T) {
env := newTestEnv(t)
defer env.close()
var exit = make(chan struct{})
defer close(exit)
// Start the automatic blockchain.
go func() {
ticker := time.NewTicker(time.Millisecond * 100)
for {
select {
case <-ticker.C:
env.backend.Commit()
case <-exit:
return
}
}
}()
// Deploy the contract if missing
drawee, err := NewChequeDrawee(env.draweeAddr, bind.NewKeyedTransactor(env.draweeKey), env.backend, env.backend, env.db)
if err != nil {
t.Fatalf("Faield to deploy contract, err: %v", err)
}
drawer, err := NewChequeDrawer(bind.NewKeyedTransactor(env.drawerKey), nil, env.drawerAddr, drawee.ContractAddr(), env.backend, env.backend, env.db)
if err != nil {
t.Fatalf("Faield to create drawer, err: %v", err)
}
drawer.keySigner = func(data []byte) ([]byte, error) {
sig, _ := crypto.Sign(data, env.drawerKey)
return sig, nil
}
// Try to withdraw money, but we have no deposit.
if err := drawer.Withdraw(context.Background()); err == nil {
t.Fatal("Failed to reject invalid withdrawal request")
}
drawer.Deposit(context.Background(), big.NewInt(200))
if err := drawer.Withdraw(context.Background()); err != nil {
t.Fatalf("Failed to open withdrawal request, err: %v", err)
}
if err := drawer.Withdraw(context.Background()); err == nil {
t.Fatal("Duplicated withdrawal request should be rejected")
}
number := drawer.WithdrawalRecord()
if number == 0 {
t.Fatal("Withdrawal record should be set")
}
// After opening the withdrawal request, we can spend this part
env.issueAndCheck(t, drawer, big.NewInt(100), ErrNotEnoughDeposit, nil, nil)
}

View file

@ -0,0 +1,38 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accountbook
import "errors"
// ErrNotEnoughDeposit is returned if the cheque drawer doesn't have enough
// balance to spend. Whenever drawee receives this error, it should emit a
// cash operation as soon as possible.
var ErrNotEnoughDeposit = errors.New("deposit is not enough")
// StaleChequeError wraps a error msg and the evidence for a stale cheque.
//
// Cheque drawer can sign the stale cheques deliberately or indeliberately.
// E.G. If the cheque db of drawer is missing, it can lead to a indeliberate
// stale cheque.
type StaleChequeError struct {
Msg string
Evidence *Cheque // The latest received cheque can be used as an evidence
}
func (err *StaleChequeError) Error() string {
return err.Msg
}

View file

@ -59,7 +59,7 @@ type LesServer interface {
APIs() []rpc.API APIs() []rpc.API
Protocols() []p2p.Protocol Protocols() []p2p.Protocol
SetBloomBitsIndexer(bbIndexer *core.ChainIndexer) SetBloomBitsIndexer(bbIndexer *core.ChainIndexer)
SetContractBackend(bind.ContractBackend) SetBackends(bind.ContractBackend, bind.DeployBackend)
} }
// Ethereum implements the Ethereum full node service. // 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. // 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. // Pass the rpc client to les server if it is enabled.
if s.lesServer != nil { if s.lesServer != nil {
s.lesServer.SetContractBackend(backend) s.lesServer.SetBackends(contract, deploy)
} }
} }

View file

@ -112,6 +112,11 @@ type Config struct {
UltraLightFraction int `toml:",omitempty"` // Percentage of trusted servers to accept an announcement 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 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 // Database options
SkipBcVersionCheck bool `toml:"-"` SkipBcVersionCheck bool `toml:"-"`
DatabaseHandles int `toml:"-"` DatabaseHandles int `toml:"-"`

View file

@ -833,6 +833,11 @@ web3._extend({
call: 'les_addBalance', call: 'les_addBalance',
params: 3 params: 3
}), }),
new web3._extend.Method({
name: 'channelInfo',
call: 'les_channelInfo',
params: 1
}),
], ],
properties: properties:
[ [
@ -848,6 +853,10 @@ web3._extend({
name: 'serverInfo', name: 'serverInfo',
getter: 'les_serverInfo' getter: 'les_serverInfo'
}), }),
new web3._extend.Property({
name: 'openedChannels',
getter: 'les_openedChannels'
}),
] ]
}); });
` `

View file

@ -20,10 +20,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"math" "math"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/les/payment"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
) )
@ -33,6 +36,7 @@ var (
errUnknownBenchmarkType = errors.New("unknown benchmark type") errUnknownBenchmarkType = errors.New("unknown benchmark type")
errBalanceOverflow = errors.New("balance overflow") errBalanceOverflow = errors.New("balance overflow")
errNoPriority = errors.New("priority too low to raise capacity") errNoPriority = errors.New("priority too low to raise capacity")
errNoPayment = errors.New("payment channel hasn't been initialized")
) )
const maxBalance = math.MaxInt64 const maxBalance = math.MaxInt64
@ -352,3 +356,19 @@ func (api *PrivateLightAPI) GetCheckpointContractAddress() (string, error) {
} }
return api.backend.oracle.config.Address.Hex(), nil 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
}

View file

@ -250,7 +250,7 @@ func (b *LesApiBackend) EventMux() *event.TypeMux {
} }
func (b *LesApiBackend) AccountManager() *accounts.Manager { func (b *LesApiBackend) AccountManager() *accounts.Manager {
return b.eth.accountManager return b.eth.am
} }
func (b *LesApiBackend) ExtRPCEnabled() bool { func (b *LesApiBackend) ExtRPCEnabled() bool {

View file

@ -508,7 +508,7 @@ func newLesServerService(ctx *adapters.ServiceContext) (node.Service, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
server, err := NewLesServer(ethereum, &config) server, err := NewLesServer(nil, ethereum, &config)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -313,7 +313,7 @@ func (h *serverHandler) measure(setup *benchmarkSetup, count int) error {
}() }()
go func() { go func() {
for i := 0; i < count; i++ { 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 errCh <- err
return return
} }

View file

@ -19,6 +19,7 @@ package les
import ( import (
"fmt" "fmt"
"sync/atomic"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "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/eth/gasprice"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/ethapi" "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/light"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
@ -60,11 +62,10 @@ type LightEthereum struct {
bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests
bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports
ApiBackend *LesApiBackend ApiBackend *LesApiBackend
eventMux *event.TypeMux eventMux *event.TypeMux
engine consensus.Engine engine consensus.Engine
accountManager *accounts.Manager netRPCService *ethapi.PublicNetAPI
netRPCService *ethapi.PublicNetAPI
} }
func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { 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, chainDb: chainDb,
peers: peers, peers: peers,
closeCh: make(chan struct{}), closeCh: make(chan struct{}),
am: ctx.AccountManager,
}, },
eventMux: ctx.EventMux, eventMux: ctx.EventMux,
reqDist: newRequestDistributor(peers, &mclock.System{}), reqDist: newRequestDistributor(peers, &mclock.System{}),
accountManager: ctx.AccountManager, engine: eth.CreateConsensusEngine(ctx, chainConfig, &config.Ethash, nil, false, chainDb),
engine: eth.CreateConsensusEngine(ctx, chainConfig, &config.Ethash, nil, false, chainDb), bloomRequests: make(chan chan *bloombits.Retrieval),
bloomRequests: make(chan chan *bloombits.Retrieval), bloomIndexer: eth.NewBloomIndexer(chainDb, params.BloomBitsBlocksClient, params.HelperTrieConfirmations),
bloomIndexer: eth.NewBloomIndexer(chainDb, params.BloomBitsBlocksClient, params.HelperTrieConfirmations), serverPool: newServerPool(chainDb, config.UltraLightServers),
serverPool: newServerPool(chainDb, config.UltraLightServers),
} }
leth.retriever = newRetrieveManager(peers, leth.reqDist, leth.serverPool) leth.retriever = newRetrieveManager(peers, leth.reqDist, leth.serverPool)
leth.relay = newLesTxRelay(peers, leth.retriever) 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) 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 return leth, nil
} }
@ -264,15 +274,42 @@ func (s *LightEthereum) Stop() error {
s.eventMux.Stop() s.eventMux.Stop()
s.serverPool.stop() s.serverPool.stop()
s.chainDb.Close() s.chainDb.Close()
if s.paymentDb != nil {
s.paymentDb.Close()
}
s.wg.Wait() s.wg.Wait()
log.Info("Light ethereum stopped") log.Info("Light ethereum stopped")
return nil return nil
} }
// SetClient sets the rpc client and binds the registrar contract. // SetClient sets the rpc client and binds the registrar contract.
func (s *LightEthereum) SetContractBackend(backend bind.ContractBackend) { func (s *LightEthereum) SetBackends(contract bind.ContractBackend, deploy bind.DeployBackend) {
if s.oracle == nil { if s.oracle != nil {
return 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)
} }

View file

@ -17,14 +17,17 @@
package les package les
import ( import (
"errors"
"math/big" "math/big"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/downloader" "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/light"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p" "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()) 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 // Execute the LES handshake
var ( var (
head = h.backend.blockchain.CurrentHeader() head = h.backend.blockchain.CurrentHeader()
@ -107,7 +114,7 @@ func (h *clientHandler) handle(p *peer) error {
number = head.Number.Uint64() number = head.Number.Uint64()
td = h.backend.blockchain.GetTd(hash, number) 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) p.Log().Debug("Light Ethereum handshake failed", "err", err)
return err return err
} }
@ -131,9 +138,40 @@ func (h *clientHandler) handle(p *peer) error {
if p.poolEntry != nil { if p.poolEntry != nil {
h.backend.serverPool.registered(p.poolEntry) 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. // Spawn a main loop to handle all incoming messages.
for { 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.Log().Debug("Light Ethereum message handling failed", "err", err)
p.fcServer.DumpLogs() p.fcServer.DumpLogs()
return err 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 // handleMsg is invoked whenever an inbound message is received from a remote
// peer. The remote connection is torn down upon returning any error. // 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 // Read the next message from the remote peer, and ensure it's fully consumed
msg, err := p.rw.ReadMsg() msg, err := p.rw.ReadMsg()
if err != nil { if err != nil {
@ -308,6 +346,10 @@ func (h *clientHandler) handleMsg(p *peer) error {
p.fcServer.ResumeFreeze(bv) p.fcServer.ResumeFreeze(bv)
p.freezeServer(false) p.freezeServer(false)
p.Log().Debug("Service resumed") p.Log().Debug("Service resumed")
case PaymentResultMsg:
if err := payment.Amend(msg.Payload); err != nil {
return err
}
default: default:
p.Log().Trace("Received invalid message", "code", msg.Code) p.Log().Trace("Received invalid message", "code", msg.Code)
return errResp(ErrInvalidMsgCode, "%v", msg.Code) return errResp(ErrInvalidMsgCode, "%v", msg.Code)

View file

@ -21,12 +21,14 @@ import (
"math/big" "math/big"
"sync" "sync"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/les/payment"
"github.com/ethereum/go-ethereum/light" "github.com/ethereum/go-ethereum/light"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discv5" "github.com/ethereum/go-ethereum/p2p/discv5"
@ -65,6 +67,13 @@ type lesCommons struct {
chtIndexer, bloomTrieIndexer *core.ChainIndexer chtIndexer, bloomTrieIndexer *core.ChainIndexer
oracle *checkpointOracle 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{} closeCh chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
} }

41
les/payment/payment.go Normal file
View file

@ -0,0 +1,41 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package payment
import (
"io"
"math/big"
)
// Payment is the way the light client pays to the les server. Payment can be
// implemented in many different ways, such as off-chain payment, on-chain payment.
// All available payments must implement the following functions.
type Payment interface {
// Pay initiates a payment to the designated payee with specified
// payemnt amount.
Pay(amount *big.Int) error
// Receive receives a payment from the payer and returns any error
// for payment processing and proving.
Receive(msg io.Reader) error
// Amend amends the local payment db based on the received message.
Amend(msg io.Reader) error
// Close exits the payment and opens the reqeust to withdraw all funds.
Close() error
}

View file

@ -0,0 +1,629 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package payment
import (
"context"
"errors"
"io"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/contracts/accountbook"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
)
var errInvalidOpt = errors.New("invalid operation")
var (
minimalDeposit = big.NewInt(1e6) // The minimal amount for single deposit operation, 1e6 gWei.
minimalDepositThreshold = big.NewInt(2e5) // The minimal amount for triggering a new deposit, 2e5 gWei
minimalCashThreshold = big.NewInt(1e6) // The minimal amount for triggering a cash operation, 1e6 gWei
minimalChallengThreshold = big.NewInt(2e5) // The minimal amount for triggering a challenge, 2e5 gWei
)
// DefaultPaymentChannelDraweeConfig is the default payment channel
// config for drawee.
var DefaultPaymentChannelDraweeConfig = &PaymentChannelConfig{
Role: PaymentDrawee,
AutoCash: true,
// The transaction fee of cash call is around 4e14 wei
// The gas cost is around 60,000, we use 10GW as the price.
// So a reasonable minimal call amount is 1e16 wei as well
// as 1e-2 ether.
AutoCashThreshold: big.NewInt(int64(1e7)), // 1e7 gWei as well as 1e-2 ether
ChallengeThreshold: big.NewInt(int64(1e6)), // 1e6 gWei as well as 1e-3 ether
}
// DefaultPaymentChannelDrawerConfig is the default payment channel
// config for drawer.
var DefaultPaymentChannelDrawerConfig = &PaymentChannelConfig{
Role: PaymentDrawer,
AutoDeposit: true,
AutoClaim: true,
// The transaction fee of deposit call is around 4e14 wei
// The gas cost is around 40,000, we use 10GW as the price.
// So a reasonable minimal deposit amount is 1e16 wei as well
// as 1e-2 ether.
DepositAmount: big.NewInt(int64(1e7)), // 1e7 gWei as well as 1e-2 ether
AutoDepositThreshold: big.NewInt(int64(1e6)), // 1e6 gWei as well as 1e-3 ether
}
// PaymentRole is the role of user in payment channel.
type PaymentRole int
const (
PaymentDrawer PaymentRole = iota
PaymentDrawee
)
// PaymentChannelConfig defines all user-selectable options for both
// drawer and drawee.
type PaymentChannelConfig struct {
// Role is the role of the user in the payment channel, either the
// payer or the payee.
Role PaymentRole
// Drawer relative options
// TrustedContract is a list of contract code hash which light client
// can trust for usage. Light client users can configure or extend it
// by themselves, but as default the in-built contract code hash is
// included(for light clients).
TrustedContracts []common.Hash
// DepositAmount is the amount deposited by each drawer for the deposit
// operation. The unit is gWei. This option is only for drawer.
DepositAmount *big.Int
// AutoDeposit is the indicator whether to perform automatic balance
// recharge. This option is only for drawer.
AutoDeposit bool
// AutoDepositThreshold is the threshold for the drawer to perform auto deposit.
// When balance is below the threshold, auto deposit is triggered. This option is
// only for drawer.
AutoDepositThreshold *big.Int
// AutoClaim is the indicator whether to perform automatic claim.
AutoClaim bool
// Drawee relative options
// AutoCash is an indicator that whether drawee to perform cheque cashing
// automatically. This option is only for drawee.
AutoCash bool
// AutoCashThreshold is the threshold for the drawee to perform auto cashing.
// When accumulated received money from single drawer exceeds the threshold,
// auto cashing is triggered. This option is only for drawee.
AutoCashThreshold *big.Int
// ChallengeThreshold is the threshold for drawee to initiate the challenge to
// the drawer's withdraw operation.
//
// If the drawer tries to withdraw the deposit that has been spent from the
// contract, the drawee can initiate the challenge. But this needs to be done
// through an on-chain transaction. So the drawer can choose to set the threshold
// and initiate the challenge when the extracted amount exceeds the value.
ChallengeThreshold *big.Int
}
// sanitize checks the provided user configurations and changes anything that's
// unreasonable or unworkable.
func (config *PaymentChannelConfig) sanitize() *PaymentChannelConfig {
conf := *config
if conf.Role != PaymentDrawer && conf.Role != PaymentDrawee {
return nil
}
if conf.Role == PaymentDrawer {
// If auto deposit is disabled, return directly.
if !conf.AutoDeposit {
return &conf
}
if conf.DepositAmount == nil || conf.DepositAmount.Cmp(minimalDeposit) < 0 {
log.Warn("Sanitizing invalid deposit amount", "provided(gWei)", conf.DepositAmount, "updated(gWei)", minimalDeposit)
conf.DepositAmount = minimalDeposit
}
if conf.AutoDepositThreshold == nil || conf.AutoDepositThreshold.Cmp(minimalDepositThreshold) < 0 {
log.Warn("Sanitizing invalid deposit threshold", "provided(gWei)", conf.AutoDepositThreshold, "updated(gWei)", minimalDepositThreshold)
conf.AutoDepositThreshold = minimalDepositThreshold
}
} else {
if conf.AutoCash && (conf.AutoCashThreshold == nil || conf.AutoCashThreshold.Cmp(minimalCashThreshold) < 0) {
log.Warn("Sanitizing invalid cash threshold", "provided(gWei)", conf.AutoCashThreshold, "updated(gWei)", minimalCashThreshold)
conf.AutoCashThreshold = minimalCashThreshold
}
if conf.ChallengeThreshold == nil || conf.ChallengeThreshold.Cmp(minimalChallengThreshold) < 0 {
log.Warn("Sanitizing invalid challenge threshold", "provided(gWei)", conf.ChallengeThreshold, "updated(gWei)", minimalChallengThreshold)
conf.ChallengeThreshold = minimalChallengThreshold
}
}
return &conf
}
// Peer defines all necessary method as the drawer or drawee.
type Peer interface {
// SendPayment sends the given cheque to the peer via network.
SendPayment(cheque *accountbook.Cheque) error
// AddBalance notifies upper-level system we have received
// the payment from the peer with specified amount.
AddBalance(amount *big.Int) error
}
// CurrentHeader retrieves the current header from the local chain.
type ChainReader interface {
CurrentHeader() *types.Header
}
type PaymentChannel struct {
config *PaymentChannelConfig
chainReader ChainReader
chanAddr common.Address
peer Peer // The peer handler of counterparty
drawer *accountbook.ChequeDrawer // Nil if payment is opened by drawee
drawee *accountbook.ChequeDrawee // Nil if payment is opened by drawer
depositCh chan struct{}
cashCh chan common.Address
closeCh chan struct{}
wg sync.WaitGroup
}
func NewPaymentChannel(config *PaymentChannelConfig, chainReader ChainReader, chanAddr common.Address, drawer *accountbook.ChequeDrawer, drawee *accountbook.ChequeDrawee, peer Peer) (*PaymentChannel, error) {
// Sanitize the config to ensure all options are valid
checked := config.sanitize()
if checked == nil {
return nil, errors.New("invalid config")
}
payment := &PaymentChannel{
config: checked,
chainReader: chainReader,
chanAddr: chanAddr,
peer: peer,
drawer: drawer,
drawee: drawee,
depositCh: make(chan struct{}),
cashCh: make(chan common.Address),
closeCh: make(chan struct{}),
}
if config.Role == PaymentDrawer {
if config.AutoDeposit {
payment.wg.Add(1)
go payment.autoDeposit()
}
if config.AutoClaim {
payment.wg.Add(1)
go payment.autoClaim()
}
} else {
if config.AutoCash {
payment.wg.Add(1)
go payment.autoCash()
}
payment.wg.Add(1)
go payment.listenWithdraw()
}
return payment, nil
}
// Pay initiates a payment to the designated payee with specified
// payemnt amount and also trigger a deposit operation if auto deposit
// is set and threshold is met.
func (c *PaymentChannel) Pay(amount *big.Int) error {
if c.config.Role != PaymentDrawer {
return errInvalidOpt
}
cheque, unspent, err := c.drawer.IssueCheque(amount)
if err != nil {
if c.config.AutoDeposit && err == accountbook.ErrNotEnoughDeposit {
select {
case c.depositCh <- struct{}{}:
case <-c.closeCh:
}
}
return err
}
if c.config.AutoDeposit && unspent.Cmp(new(big.Int).Mul(c.config.AutoDepositThreshold, big.NewInt(params.GWei))) <= 0 {
select {
case c.depositCh <- struct{}{}:
case <-c.closeCh:
}
}
log.Info("Issued payment", "amount", amount, "channel", c.chanAddr)
return c.peer.SendPayment(cheque)
}
// Receive receives a payment from the payer and returns any error
// for payment processing and proving.
func (c *PaymentChannel) Receive(msg io.Reader) error {
if c.config.Role != PaymentDrawee {
return errInvalidOpt
}
var cheque accountbook.Cheque
if err := rlp.Decode(msg, &cheque); err != nil {
return err
}
amount, unpaid, err := c.drawee.AddCheque(&cheque)
if err != nil {
return err
}
if c.config.AutoCash && unpaid.Cmp(new(big.Int).Mul(c.config.AutoCashThreshold, big.NewInt(params.GWei))) >= 0 {
select {
case c.cashCh <- cheque.Drawer:
case <-c.closeCh:
}
}
return c.peer.AddBalance(amount)
}
// Amend amends the local cheque db of drawer with externally provided cheque
// signed by drawer itself.
func (c *PaymentChannel) Amend(msg io.Reader) error {
if c.config.Role != PaymentDrawer {
return errInvalidOpt
}
var serr accountbook.StaleChequeError
if err := rlp.Decode(msg, &serr); err != nil {
return err
}
return c.drawer.Amend(serr.Evidence)
}
// Close exits the payment and opens the reqeust to withdraw all funds.
func (c *PaymentChannel) Close() error {
if c.config.Role != PaymentDrawer {
return errInvalidOpt
}
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
defer cancelFn()
if err := c.drawer.Withdraw(ctx); err != nil {
log.Info("Failed to open withdraw request", "error", err)
return err
}
return nil
}
func (c *PaymentChannel) autoCash() {
log.Info("Enable auto cash", "channel", c.chanAddr, "threshold", c.config.AutoCashThreshold)
defer c.wg.Done()
var (
done chan struct{} // Non-nil if cash routine is active.
cash = func(drawer common.Address, done chan struct{}) {
defer func() { done <- struct{}{} }()
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
defer cancelFn()
if err := c.drawee.Cash(ctx, drawer, true); err != nil {
log.Info("Failed to cash payment", "drawer", drawer, "error", err)
} else {
log.Info("Succeed to cash payment", "drawer", drawer)
}
}
)
for {
select {
case addr := <-c.cashCh:
if done == nil {
done = make(chan struct{})
go cash(addr, done)
}
case <-done:
done = nil
case <-c.closeCh:
return
}
}
}
func (c *PaymentChannel) autoDeposit() {
log.Info("Enable auto deposit", "channel", c.chanAddr, "amount", c.config.DepositAmount)
defer c.wg.Done()
var (
done chan struct{} // Non-nil if deposit routine is active.
deposit = func(done chan struct{}) {
defer func() { done <- struct{}{} }()
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
defer cancelFn()
status, err := c.drawer.Deposit(ctx, new(big.Int).Mul(c.config.DepositAmount, big.NewInt(params.GWei)))
if err != nil || !status {
log.Info("Failed to deposit", "channel", c.chanAddr, "amount(gWei)", c.config.DepositAmount, "error", err)
} else {
log.Info("Succeed to deposit", "channel", c.chanAddr, "amount(gWei)", c.config.DepositAmount)
}
}
)
for {
select {
case <-c.depositCh:
if done == nil {
done = make(chan struct{})
go deposit(done)
}
case <-done:
done = nil
case <-c.closeCh:
return
}
}
}
func (c *PaymentChannel) autoClaim() {
defer c.wg.Done()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
var (
done chan struct{} // Non-nil if deposit routine is active.
claim = func(done chan struct{}) {
defer func() { done <- struct{}{} }()
defer c.drawer.ResetWithdrawlRecord()
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
defer cancelFn()
if status, err := c.drawer.Claim(ctx); err != nil || !status {
log.Info("Failed to claim", "channel", c.chanAddr, "error", err)
} else {
log.Info("Succeed to claim", "channel", c.chanAddr)
}
}
)
for {
select {
case <-ticker.C:
if done != nil {
continue
}
createdAt := c.drawer.WithdrawalRecord()
if createdAt != 0 {
local := c.chainReader.CurrentHeader()
if local.Number.Uint64() > createdAt && local.Number.Uint64()-createdAt > accountbook.ChallengeTimeWindow {
done = make(chan struct{})
go claim(done)
}
}
case <-done:
done = nil
case <-c.closeCh:
return
}
}
}
func (c *PaymentChannel) listenWithdraw() {
defer c.wg.Done()
sub, channel, err := c.drawee.ListenWithdraw()
if err != nil {
log.Info("Failed to subscribe withdraw event", "error", err)
return
}
defer sub.Unsubscribe()
for {
select {
case event := <-channel:
amount := event.Amount
if amount == nil || amount.Cmp(new(big.Int).Mul(c.config.ChallengeThreshold, big.NewInt(params.GWei))) < 0 {
continue
}
_, err := c.drawee.Unspent(event.Addr)
if err != nil && err != accountbook.ErrNotEnoughDeposit {
continue
}
// Drawer tries to withdraw spent money, challenge him.
go func() {
if err := c.drawee.Cash(context.Background(), event.Addr, false); err != nil {
log.Info("Failed to challenge", "drawer", event.Addr, "error", err)
} else {
log.Info("Succeed to challenge", "drawer", event.Addr, "error", err)
}
}()
case <-c.closeCh:
return
}
}
}
func (c *PaymentChannel) exit() {
close(c.closeCh)
c.wg.Wait()
return
}
type PaymentChannelManager struct {
config *PaymentChannelConfig
chainReader ChainReader
localAddr common.Address
txSigner *bind.TransactOpts
chequeSigner func(data []byte) ([]byte, error)
db ethdb.Database
lock sync.RWMutex
// payments are all established channels. For payment drawer, the key of payment
// map is channel contract address, otherwise the key refers to drawer's address.
payments map[common.Address]*PaymentChannel
drawee *accountbook.ChequeDrawee // Nil if manager is opened by drawer
// Backends used to interact with the underlying payment contract
cBackend bind.ContractBackend
dBackend bind.DeployBackend
}
// NewPaymentChannel initializes a one-to-one payment channel for both
// drawer and drawee.
func NewPaymentChannelManager(config *PaymentChannelConfig, chainReader ChainReader, txSigner *bind.TransactOpts, chequeSigner func(digestHash []byte) ([]byte, error), localAddr common.Address, cBackend bind.ContractBackend, dBackend bind.DeployBackend, db ethdb.Database) (*PaymentChannelManager, error) {
c := &PaymentChannelManager{
config: config,
chainReader: chainReader,
localAddr: localAddr,
txSigner: txSigner,
chequeSigner: chequeSigner,
db: db,
cBackend: cBackend,
dBackend: dBackend,
payments: make(map[common.Address]*PaymentChannel),
}
// Drawer has to initialize channel here if contract
// hasn't been deployed yet.
if c.config.Role == PaymentDrawee {
drawee, err := accountbook.NewChequeDrawee(c.localAddr, txSigner, cBackend, dBackend, db)
if err != nil {
return nil, err
}
c.drawee = drawee
}
return c, nil
}
// OpenChannel establishes a new payment channel for new customer or new vendor.
// If we are payment drawer, the addr refers to the payment channel contract addr,
// otherwise, the addr refers to drawer's address.
func (c *PaymentChannelManager) OpenChannel(addr common.Address, peer Peer) (Payment, error) {
c.lock.Lock()
defer c.lock.Unlock()
// Filter all duplicated channels.
if _, exist := c.payments[addr]; exist {
return nil, errors.New("duplicated payment channel")
}
var err error
var channel *PaymentChannel
if c.config.Role == PaymentDrawer {
// We are payment drawer, establish a outgoing channel with
// specified contract address and counterparty peer.
drawer, err := accountbook.NewChequeDrawer(c.txSigner, c.chequeSigner, c.localAddr, addr, c.cBackend, c.dBackend, c.db)
if err != nil {
return nil, err
}
channel, err = NewPaymentChannel(c.config, c.chainReader, addr, drawer, nil, peer)
if err != nil {
return nil, err
}
c.payments[addr] = channel
} else {
// We are payment drawee, establish a incoming channel with
// specified counterparty address and peer.
channel, err = NewPaymentChannel(c.config, c.chainReader, c.drawee.ContractAddr(), nil, c.drawee, peer)
if err != nil {
return nil, err
}
c.payments[addr] = channel
}
return channel, nil
}
// CloseChannel closes a channel with given address. If we are payment drawer,
// the addr refers to the payment channel contract addr, otherwise, the addr
// refers to drawer's address.
func (c *PaymentChannelManager) CloseChannel(addr common.Address) error {
c.lock.Lock()
defer c.lock.Unlock()
if payment, exist := c.payments[addr]; !exist {
return errors.New("channel doesn't exist")
} else {
payment.exit()
delete(c.payments, addr)
}
return nil
}
// VerifyChannel ensures the code of payment channel is trusted.
func (c *PaymentChannelManager) VerifyChannel(chanAddr common.Address) error {
c.lock.RLock()
defer c.lock.RUnlock()
if c.config.Role == PaymentDrawee {
return nil
}
if len(c.config.TrustedContracts) == 0 {
return nil
}
payment, exist := c.payments[chanAddr]
if !exist {
return errors.New("channel doesn't exist")
}
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute*5)
defer cancelFn()
hash, err := payment.drawer.CodeHash(ctx)
if err != nil {
return err
}
for _, h := range c.config.TrustedContracts {
if h == hash {
return nil
}
}
return errors.New("untrusted contract")
}
// ChannelAddress returns all established channel addresses.
func (c *PaymentChannelManager) ChannelAddresses() []common.Address {
if c.config.Role == PaymentDrawer {
var addresses []common.Address
c.lock.RLock()
defer c.lock.RUnlock()
for addr := range c.payments {
addresses = append(addresses, addr)
}
return addresses
}
return []common.Address{c.drawee.ContractAddr()}
}
// ChannelInfo includes all basic information about the specified channel.
type ChannelInfo struct {
Received []*accountbook.Cheque
Payed *big.Int
}
// ChannelInfo returns all basic information about the channel.
func (c *PaymentChannelManager) ChannelInfo(chanAddr common.Address) ChannelInfo {
c.lock.RLock()
defer c.lock.RUnlock()
var info ChannelInfo
if c.config.Role == PaymentDrawer {
payment, exist := c.payments[chanAddr]
if !exist {
return info
}
info.Payed = payment.drawer.Payed()
} else {
info.Received = c.drawee.ListCheques()
}
return info
}

View file

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock" "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"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
@ -117,6 +118,10 @@ type peer struct {
onlyAnnounce bool onlyAnnounce bool
chainSince, chainRecent uint64 chainSince, chainRecent uint64
stateSince, stateRecent 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 { 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) 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 // ReplyBlockHeaders creates a reply with a batch of block headers
func (p *peer) ReplyBlockHeaders(reqID uint64, headers []*types.Header) *reply { func (p *peer) ReplyBlockHeaders(reqID uint64, headers []*types.Header) *reply {
data, _ := rlp.EncodeToBytes(headers) 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, // Handshake executes the les protocol handshake, negotiating version number,
// network IDs, difficulties, head and genesis blocks. // 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() p.lock.Lock()
defer p.lock.Unlock() 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) 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 { } else {
// Add some client-specific handshake fields // Add some client-specific handshake fields
p.announceType = announceTypeSimple p.announceType = announceTypeSimple
@ -630,6 +654,7 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis
p.announceType = announceTypeSigned p.announceType = announceTypeSigned
} }
send = send.add("announceType", p.announceType) send = send.add("announceType", p.announceType)
send = send.add("payment/payer", addr)
} }
recvList, err := p.sendReceiveHandshake(send) 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.announceType = announceTypeSimple
} }
p.fcClient = flowcontrol.NewClientNode(server.fcManager, server.defParams) p.fcClient = flowcontrol.NewClientNode(server.fcManager, server.defParams)
recv.get("payment/payer", &p.payerAddr)
} else { } else {
if recv.get("serveChainSince", &p.chainSince) != nil { if recv.get("serveChainSince", &p.chainSince) != nil {
p.onlyAnnounce = true 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/value", &p.checkpoint)
recv.get("checkpoint/registerHeight", &p.checkpointNumber) recv.get("checkpoint/registerHeight", &p.checkpointNumber)
recv.get("payment/paymentChannel", &p.paymentChannel)
if !p.onlyAnnounce { if !p.onlyAnnounce {
for msgCode := range reqAvgTimeCost { for msgCode := range reqAvgTimeCost {
if p.fcCosts[msgCode] == nil { if p.fcCosts[msgCode] == nil {

View file

@ -83,7 +83,7 @@ func TestPeerHandshakeSetAnnounceTypeToAnnounceTypeSignedForTrustedPeer(t *testi
}, },
network: NetworkId, network: NetworkId,
} }
err := p.Handshake(td, hash, headNum, genesis, nil) err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
if err != nil { if err != nil {
t.Fatalf("Handshake error: %s", err) t.Fatalf("Handshake error: %s", err)
} }
@ -123,7 +123,7 @@ func TestPeerHandshakeAnnounceTypeSignedForTrustedPeersPeerNotInTrusted(t *testi
}, },
network: NetworkId, network: NetworkId,
} }
err := p.Handshake(td, hash, headNum, genesis, nil) err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -150,7 +150,7 @@ func TestPeerHandshakeDefaultAllRequests(t *testing.T) {
network: NetworkId, network: NetworkId,
} }
err := p.Handshake(td, hash, headNum, genesis, s) err := p.Handshake(td, hash, headNum, genesis, s, common.Address{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -188,7 +188,7 @@ func TestPeerHandshakeServerSendOnlyAnnounceRequestsHeaders(t *testing.T) {
network: NetworkId, network: NetworkId,
} }
err := p.Handshake(td, hash, headNum, genesis, s) err := p.Handshake(td, hash, headNum, genesis, s, common.Address{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -214,7 +214,7 @@ func TestPeerHandshakeClientReceiveOnlyAnnounceRequestsHeaders(t *testing.T) {
trusted: true, trusted: true,
} }
err := p.Handshake(td, hash, headNum, genesis, nil) err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -242,7 +242,7 @@ func TestPeerHandshakeClientReturnErrorOnUselessPeer(t *testing.T) {
network: NetworkId, network: NetworkId,
} }
err := p.Handshake(td, hash, headNum, genesis, nil) err := p.Handshake(td, hash, headNum, genesis, nil, common.Address{})
if err == nil { if err == nil {
t.FailNow() t.FailNow()
} }

View file

@ -33,17 +33,18 @@ import (
const ( const (
lpv2 = 2 lpv2 = 2
lpv3 = 3 lpv3 = 3
lpv4 = 4
) )
// Supported versions of the les protocol (first is primary) // Supported versions of the les protocol (first is primary)
var ( var (
ClientProtocolVersions = []uint{lpv2, lpv3} ClientProtocolVersions = []uint{lpv2, lpv3, lpv4}
ServerProtocolVersions = []uint{lpv2, lpv3} ServerProtocolVersions = []uint{lpv2, lpv3, lpv4}
AdvertiseProtocolVersions = []uint{lpv2} // clients are searching for the first advertised protocol in the list AdvertiseProtocolVersions = []uint{lpv2} // clients are searching for the first advertised protocol in the list
) )
// Number of implemented message corresponding to different protocol versions. // 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 ( const (
NetworkId = 1 NetworkId = 1
@ -74,6 +75,9 @@ const (
// Protocol messages introduced in LPV3 // Protocol messages introduced in LPV3
StopMsg = 0x16 StopMsg = 0x16
ResumeMsg = 0x17 ResumeMsg = 0x17
// Protocol messages introduced in LPV4
PaymentMsg = 0x18
PaymentResultMsg = 0x19
) )
type requestInfo struct { type requestInfo struct {

View file

@ -18,15 +18,20 @@ package les
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "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/common/mclock"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/les/flowcontrol" "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/light"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discv5" "github.com/ethereum/go-ethereum/p2p/discv5"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
@ -42,6 +47,7 @@ type LesServer struct {
handler *serverHandler handler *serverHandler
lesTopics []discv5.Topic lesTopics []discv5.Topic
privateKey *ecdsa.PrivateKey privateKey *ecdsa.PrivateKey
synced func() bool
// Flow control and capacity management // Flow control and capacity management
fcManager *flowcontrol.ClientManager fcManager *flowcontrol.ClientManager
@ -55,7 +61,7 @@ type LesServer struct {
threadsBusy int // Request serving threads count when system is busy(block insertion). 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. // Collect les protocol version information supported by local node.
lesTopics := make([]discv5.Topic, len(AdvertiseProtocolVersions)) lesTopics := make([]discv5.Topic, len(AdvertiseProtocolVersions))
for i, pv := range 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), chtIndexer: light.NewChtIndexer(e.ChainDb(), nil, params.CHTFrequency, params.HelperTrieProcessConfirmations),
bloomTrieIndexer: light.NewBloomTrieIndexer(e.ChainDb(), nil, params.BloomBitsBlocks, params.BloomTrieFrequency), bloomTrieIndexer: light.NewBloomTrieIndexer(e.ChainDb(), nil, params.BloomBitsBlocks, params.BloomTrieFrequency),
closeCh: make(chan struct{}), closeCh: make(chan struct{}),
am: ctx.AccountManager,
}, },
archiveMode: e.ArchiveMode(), archiveMode: e.ArchiveMode(),
lesTopics: lesTopics, 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), servingQueue: newServingQueue(int64(time.Millisecond*10), float64(config.LightServ)/100),
threadsBusy: config.LightServ/100 + 1, threadsBusy: config.LightServ/100 + 1,
threadsIdle: threads, threadsIdle: threads,
synced: e.Synced,
} }
srv.handler = newServerHandler(srv, e.BlockChain(), e.ChainDb(), e.TxPool(), e.Synced) srv.handler = newServerHandler(srv, e.BlockChain(), e.ChainDb(), e.TxPool(), e.Synced)
srv.costTracker, srv.minCapacity = newCostTracker(e.ChainDb(), config) 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) "chtroot", checkpoint.CHTRoot, "bloomroot", checkpoint.BloomRoot)
} }
srv.chtIndexer.Start(e.BlockChain()) 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 return srv, nil
} }
@ -203,6 +220,9 @@ func (s *LesServer) Stop() {
// Note, bloom trie indexer is closed by parent bloombits indexer. // Note, bloom trie indexer is closed by parent bloombits indexer.
s.chtIndexer.Close() s.chtIndexer.Close()
if s.paymentDb != nil {
s.paymentDb.Close()
}
s.wg.Wait() s.wg.Wait()
log.Info("Les server stopped") 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. // SetClient sets the rpc client and starts running checkpoint contract if it is not yet watched.
func (s *LesServer) SetContractBackend(backend bind.ContractBackend) { func (s *LesServer) SetBackends(cbackend bind.ContractBackend, dbackend bind.DeployBackend) {
if s.oracle == nil { if s.oracle != nil {
return 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 // capacityManagement starts an event handler loop that updates the recharge curve of

View file

@ -24,6 +24,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/contracts/accountbook"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
@ -31,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/les/payment"
"github.com/ethereum/go-ethereum/light" "github.com/ethereum/go-ethereum/light"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "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()) 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 // Execute the LES handshake
var ( var (
head = h.blockchain.CurrentHeader() head = h.blockchain.CurrentHeader()
@ -121,7 +127,7 @@ func (h *serverHandler) handle(p *peer) error {
number = head.Number.Uint64() number = head.Number.Uint64()
td = h.blockchain.GetTd(hash, number) 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) p.Log().Debug("Light Ethereum handshake failed", "err", err)
return err return err
} }
@ -150,7 +156,16 @@ func (h *serverHandler) handle(p *peer) error {
clientConnectionGauge.Update(int64(h.server.peers.Len())) clientConnectionGauge.Update(int64(h.server.peers.Len()))
connectionTimer.Update(time.Duration(mclock.Now() - connectedAt)) 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. // Spawn a main loop to handle all incoming messages.
for { for {
select { select {
@ -159,7 +174,7 @@ func (h *serverHandler) handle(p *peer) error {
return err return err
default: 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) p.Log().Debug("Light Ethereum message handling failed", "err", err)
return 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 // handleMsg is invoked whenever an inbound message is received from a remote
// peer. The remote connection is torn down upon returning any error. // 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 // Read the next message from the remote peer, and ensure it's fully consumed
msg, err := p.rw.ReadMsg() msg, err := p.rw.ReadMsg()
if err != nil { 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: default:
p.Log().Trace("Received invalid message", "code", msg.Code) p.Log().Trace("Received invalid message", "code", msg.Code)
clientErrorMeter.Mark(1) clientErrorMeter.Mark(1)