From 9493109f2be4507605e6b17e406bf8fd147ab3c8 Mon Sep 17 00:00:00 2001 From: Wei-Ning Huang Date: Sun, 17 Mar 2019 09:12:50 +0800 Subject: dex: implement recovery mechanism (#258) * dex: implement recovery mechanism The DEXON recovery protocol allows us to use the Ethereum blockchain as a fallback consensus chain to coordinate recovery. * fix --- dex/backend.go | 8 +- dex/blockproposer.go | 30 +++- dex/config.go | 3 + dex/governance.go | 13 ++ dex/recovery.go | 484 +++++++++++++++++++++++++++++++++++++++++++++++++++ dex/recovery_test.go | 43 +++++ 6 files changed, 576 insertions(+), 5 deletions(-) create mode 100644 dex/recovery.go create mode 100644 dex/recovery_test.go (limited to 'dex') diff --git a/dex/backend.go b/dex/backend.go index 056a6d221..6ee1a5fa1 100644 --- a/dex/backend.go +++ b/dex/backend.go @@ -21,6 +21,7 @@ import ( "fmt" "time" + "github.com/dexon-foundation/dexon-consensus/core/syncer" "github.com/dexon-foundation/dexon/accounts" "github.com/dexon-foundation/dexon/consensus" "github.com/dexon-foundation/dexon/consensus/dexcon" @@ -180,7 +181,12 @@ func New(ctx *node.ServiceContext, config *Config) (*Dexon, error) { dex.protocolManager = pm dex.network = NewDexconNetwork(pm) - dex.bp = NewBlockProposer(dex, dMoment) + recovery := NewRecovery(chainConfig.Recovery, config.RecoveryNetworkRPC, + dex.governance, config.PrivateKey) + watchCat := syncer.NewWatchCat(recovery, dex.governance, 10*time.Second, + time.Duration(chainConfig.Recovery.Timeout)*time.Second, log.Root()) + + dex.bp = NewBlockProposer(dex, watchCat, dMoment) return dex, nil } diff --git a/dex/blockproposer.go b/dex/blockproposer.go index ad8b4c2b0..6c1de818a 100644 --- a/dex/blockproposer.go +++ b/dex/blockproposer.go @@ -24,16 +24,18 @@ type blockProposer struct { syncing int32 proposing int32 dex *Dexon + watchCat *syncer.WatchCat dMoment time.Time wg sync.WaitGroup stopCh chan struct{} } -func NewBlockProposer(dex *Dexon, dMoment time.Time) *blockProposer { +func NewBlockProposer(dex *Dexon, watchCat *syncer.WatchCat, dMoment time.Time) *blockProposer { return &blockProposer{ - dex: dex, - dMoment: dMoment, + dex: dex, + watchCat: watchCat, + dMoment: dMoment, } } @@ -116,9 +118,21 @@ func (b *blockProposer) syncConsensus() (*dexCore.Consensus, error) { consensusSync := syncer.NewConsensus(b.dMoment, b.dex.app, b.dex.governance, db, b.dex.network, privkey, log.Root()) + // Start the watchCat. + log.Info("Starting sync watchCat ...") + b.watchCat.Start() + + // Feed the current block we have in local blockchain. + cb := b.dex.blockchain.CurrentBlock() + var block coreTypes.Block + if err := rlp.DecodeBytes(cb.Header().DexconMeta, &block); err != nil { + panic(err) + } + b.watchCat.Feed(block.Position) + blocksToSync := func(coreHeight, height uint64) []*coreTypes.Block { var blocks []*coreTypes.Block - for len(blocks) < 1024 && coreHeight < height { + for coreHeight < height { var block coreTypes.Block b := b.dex.blockchain.GetBlockByNumber(coreHeight + 1) if err := rlp.DecodeBytes(b.Header().DexconMeta, &block); err != nil { @@ -143,6 +157,7 @@ Loop: if len(blocks) == 0 { break Loop } + b.watchCat.Feed(blocks[len(blocks)-1].Position) log.Debug("Filling compaction chain", "num", len(blocks), "first", blocks[0].Finalization.Height, @@ -172,6 +187,8 @@ ListenLoop: select { case ev := <-ch: blocks := blocksToSync(coreHeight, ev.Block.NumberU64()) + b.watchCat.Feed(blocks[len(blocks)-1].Position) + if len(blocks) > 0 { log.Debug("Filling compaction chain", "num", len(blocks), "first", blocks[0].Finalization.Height, @@ -193,8 +210,13 @@ ListenLoop: case <-b.stopCh: log.Debug("Early stop, before consensus core can run") return nil, errors.New("early stop") + case <-b.watchCat.Meow(): + log.Info("WatchCat signaled to stop syncing") + consensusSync.ForceSync(true) + break ListenLoop } } + b.watchCat.Stop() return consensusSync.GetSyncedConsensus() } diff --git a/dex/config.go b/dex/config.go index 7661907cc..d218b35e2 100644 --- a/dex/config.go +++ b/dex/config.go @@ -126,4 +126,7 @@ type Config struct { // Indexer config Indexer indexer.Config + + // Recovery network RPC + RecoveryNetworkRPC string } diff --git a/dex/governance.go b/dex/governance.go index d9cf8fb65..35c5b4174 100644 --- a/dex/governance.go +++ b/dex/governance.go @@ -263,6 +263,19 @@ func (d *DexconGovernance) NotarySet(round uint64) (map[string]struct{}, error) return r, nil } +func (d *DexconGovernance) NotarySetAddresses(round uint64) (map[common.Address]struct{}, error) { + notarySet, err := d.nodeSetCache.GetNotarySet(round) + if err != nil { + return nil, err + } + + r := make(map[common.Address]struct{}, len(notarySet)) + for id := range notarySet { + r[vm.IdToAddress(id)] = struct{}{} + } + return r, nil +} + func (d *DexconGovernance) DKGSet(round uint64) (map[string]struct{}, error) { dkgSet, err := d.nodeSetCache.GetDKGSet(round) if err != nil { diff --git a/dex/recovery.go b/dex/recovery.go new file mode 100644 index 000000000..cfc8ae203 --- /dev/null +++ b/dex/recovery.go @@ -0,0 +1,484 @@ +// Copyright 2018 The dexon-consensus Authors +// This file is part of the dexon-consensus library. +// +// The dexon-consensus 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 dexon-consensus 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 dexon-consensus library. If not, see +// . + +package dex + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math/big" + "strconv" + "strings" + + "github.com/dexon-foundation/dexon/accounts/abi" + "github.com/dexon-foundation/dexon/common" + "github.com/dexon-foundation/dexon/core/types" + "github.com/dexon-foundation/dexon/crypto" + "github.com/dexon-foundation/dexon/params" + "github.com/dexon-foundation/dexon/rlp" + "github.com/onrik/ethrpc" +) + +const numConfirmation = 1 + +const recoveryABI = ` +[ + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "address" + } + ], + "name": "voted", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isOwner", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "depositValue", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "uint256" + } + ], + "name": "votes", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "withdrawable", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "height", + "type": "uint256" + }, + { + "indexed": false, + "name": "voter", + "type": "address" + } + ], + "name": "VotedForRecovery", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "constant": false, + "inputs": [ + { + "name": "DepositValue", + "type": "uint256" + } + ], + "name": "setDeposit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "height", + "type": "uint256" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "refund", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "height", + "type": "uint256" + } + ], + "name": "voteForSkipBlock", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "height", + "type": "uint256" + } + ], + "name": "numVotes", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] +` + +var abiObject abi.ABI + +func init() { + var err error + abiObject, err = abi.JSON(strings.NewReader(recoveryABI)) + if err != nil { + panic(err) + } +} + +type Recovery struct { + gov *DexconGovernance + contract common.Address + confirmation int + privateKey *ecdsa.PrivateKey + nodeAddress common.Address + client *ethrpc.EthRPC +} + +func NewRecovery(config *params.RecoveryConfig, networkRPC string, + gov *DexconGovernance, privKey *ecdsa.PrivateKey) *Recovery { + client := ethrpc.New(networkRPC) + return &Recovery{ + gov: gov, + contract: config.Contract, + confirmation: config.Confirmation, + privateKey: privKey, + nodeAddress: crypto.PubkeyToAddress(privKey.PublicKey), + client: client, + } +} + +func (r *Recovery) genVoteForSkipBlockTx(height uint64) (*types.Transaction, error) { + netVersion, err := r.client.NetVersion() + if err != nil { + return nil, err + } + + networkID, err := strconv.Atoi(netVersion) + if err != nil { + return nil, err + } + + data, err := abiObject.Pack("depositValue") + if err != nil { + return nil, err + } + + res, err := r.client.EthCall(ethrpc.T{ + From: r.nodeAddress.String(), + To: r.contract.String(), + Data: "0x" + hex.EncodeToString(data), + }, "latest") + if err != nil { + return nil, err + } + + resBytes, err := hex.DecodeString(res[2:]) + if err != nil { + return nil, err + } + + var depositValue *big.Int + err = abiObject.Unpack(&depositValue, "depositValue", resBytes) + if err != nil { + return nil, err + } + + data, err = abiObject.Pack("voteForSkipBlock", new(big.Int).SetUint64(height)) + if err != nil { + return nil, err + } + + gasPrice, err := r.client.EthGasPrice() + if err != nil { + return nil, err + } + + nonce, err := r.client.EthGetTransactionCount(r.nodeAddress.String(), "pending") + if err != nil { + return nil, err + } + + // Increase gasPrice to 3 times of suggested gas price to make sure it will + // be included in time. + useGasPrice := new(big.Int).Mul(&gasPrice, big.NewInt(3)) + + tx := types.NewTransaction( + uint64(nonce), + r.contract, + depositValue, + uint64(100000), + useGasPrice, + data) + + signer := types.NewEIP155Signer(big.NewInt(int64(networkID))) + return types.SignTx(tx, signer, r.privateKey) +} + +func (r *Recovery) ProposeSkipBlock(height uint64) error { + tx, err := r.genVoteForSkipBlockTx(height) + if err != nil { + return err + } + + txData, err := rlp.EncodeToBytes(tx) + if err != nil { + return err + } + _, err = r.client.EthSendRawTransaction("0x" + hex.EncodeToString(txData)) + return err +} + +func (r *Recovery) Votes(height uint64) (uint64, error) { + data, err := abiObject.Pack("numVotes", new(big.Int).SetUint64(height)) + if err != nil { + return 0, err + } + + bn, err := r.client.EthBlockNumber() + if err != nil { + return 0, err + } + + snapshotHeight := bn - numConfirmation + + res, err := r.client.EthCall(ethrpc.T{ + From: r.nodeAddress.String(), + To: r.contract.String(), + Data: "0x" + hex.EncodeToString(data), + }, fmt.Sprintf("0x%x", snapshotHeight)) + if err != nil { + return 0, err + } + + resBytes, err := hex.DecodeString(res[2:]) + if err != nil { + return 0, err + } + + votes := new(big.Int) + err = abiObject.Unpack(&votes, "numVotes", resBytes) + if err != nil { + return 0, err + } + + notarySet, err := r.gov.NotarySetAddresses(r.gov.Round()) + if err != nil { + return 0, err + } + + count := uint64(0) + + for i := uint64(0); i < votes.Uint64(); i++ { + data, err = abiObject.Pack( + "votes", new(big.Int).SetUint64(height), new(big.Int).SetUint64(i)) + if err != nil { + return 0, err + } + + res, err = r.client.EthCall(ethrpc.T{ + From: r.nodeAddress.String(), + To: r.contract.String(), + Data: "0x" + hex.EncodeToString(data), + }, fmt.Sprintf("0x%x", snapshotHeight)) + if err != nil { + return 0, err + } + + resBytes, err := hex.DecodeString(res[2:]) + if err != nil { + return 0, err + } + + var addr common.Address + err = abiObject.Unpack(&addr, "votes", resBytes) + if err != nil { + return 0, err + } + + if _, ok := notarySet[addr]; ok { + count += 1 + } + } + return count, nil +} diff --git a/dex/recovery_test.go b/dex/recovery_test.go new file mode 100644 index 000000000..8c039db35 --- /dev/null +++ b/dex/recovery_test.go @@ -0,0 +1,43 @@ +// Copyright 2018 The dexon-consensus Authors +// This file is part of the dexon-consensus library. +// +// The dexon-consensus 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 dexon-consensus 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 dexon-consensus library. If not, see +// . + +package dex + +import ( + "testing" + + "github.com/dexon-foundation/dexon/common" + "github.com/dexon-foundation/dexon/crypto" + "github.com/dexon-foundation/dexon/params" +) + +func TestRecoveryVoteTxGeneration(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate keypair: %v", err) + } + + r := NewRecovery(¶ms.RecoveryConfig{ + Contract: common.HexToAddress("f675c0e9bf4b949f50dcec5b224a70f0361d4680"), + Timeout: 30, + Confirmation: 1, + }, "https://rinkeby.infura.io", nil, key) + _, err = r.genVoteForSkipBlockTx(0) + if err != nil { + t.Fatalf("failed to generate voteForSkipBlock tx: %v", err) + } +} -- cgit v1.2.3