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