From 2818ad97f5b302f76e3296195bf8daae1868c435 Mon Sep 17 00:00:00 2001
From: Wei-Ning Huang <w@dexon.org>
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
+// <http://www.gnu.org/licenses/>.
+
+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
+// <http://www.gnu.org/licenses/>.
+
+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(&params.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