aboutsummaryrefslogtreecommitdiffstats
path: root/dex
diff options
context:
space:
mode:
authorWei-Ning Huang <w@dexon.org>2019-03-17 09:12:50 +0800
committerWei-Ning Huang <w@dexon.org>2019-04-09 21:32:58 +0800
commit2818ad97f5b302f76e3296195bf8daae1868c435 (patch)
tree0e055a8dc82f8a473f877400074dffe7b811090c /dex
parent9c9073db149d89eb31dc7c68d440b359f42fe8e9 (diff)
downloaddexon-2818ad97f5b302f76e3296195bf8daae1868c435.tar.gz
dexon-2818ad97f5b302f76e3296195bf8daae1868c435.tar.zst
dexon-2818ad97f5b302f76e3296195bf8daae1868c435.zip
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
Diffstat (limited to 'dex')
-rw-r--r--dex/backend.go8
-rw-r--r--dex/blockproposer.go30
-rw-r--r--dex/config.go3
-rw-r--r--dex/governance.go13
-rw-r--r--dex/recovery.go484
-rw-r--r--dex/recovery_test.go43
6 files changed, 576 insertions, 5 deletions
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)
+ }
+}