Quellcode durchsuchen

bridge: listen to eth lockups and aggregate signatures from all nodes

Improved devnet setup to generate deterministic node and guardian keys.

Devnet setup routine that configures a dynamic guardian set on Ethereum.

Configurable number of nodes in Tiltfile.
Leo vor 5 Jahren
Ursprung
Commit
d6ef9c932c

+ 15 - 1
DEVELOP.md

@@ -19,6 +19,20 @@ This should work on Linux, MacOS and possibly even Windows.
 After installing all dependencies, just run `tilt up --update-mode=exec`. 
 Whenever you modify a file, the devnet is automatically rebuilt and a rolling update is done.
 
-Watch pod status in your cluster: `kubectl get pod -A -w`.
+Specify number of guardians nodes to run (default is five):
+
+    tilt up --update-mode=exec -- --num=10
+
+Watch pod status in your cluster:
+
+    kubectl get pod -A -w
+    
+Get logs for single guardian node:
+
+    kubectl logs guardian-0
+
+Generate test ETH lockups once the cluster is up:
+
+    kubectl exec -it -c tests eth-devnet-0 -- npx truffle exec src/send-lockups.js
 
 Once you're done, press Ctrl-C. Run `tilt down` to tear down the devnet.

+ 18 - 2
Tiltfile

@@ -1,3 +1,7 @@
+config.define_string("num", False, "Number of guardian nodes to run")
+cfg = config.parse()
+num_guardians = int(cfg.get("num", "5"))
+
 # protos
 
 local_resource(
@@ -14,7 +18,20 @@ docker_build(
     dockerfile = "bridge/Dockerfile",
 )
 
-k8s_yaml("devnet/bridge.yaml")
+def build_bridge_yaml():
+    bridge_yaml = read_yaml_stream("devnet/bridge.yaml")
+
+    for obj in bridge_yaml:
+        if obj['kind'] == 'StatefulSet' and obj['metadata']['name'] == 'guardian':
+            obj['spec']['replicas'] = num_guardians
+            container = obj['spec']['template']['spec']['containers'][0]
+            if container['name'] != 'guardiand':
+                fail("container 0 is not guardiand")
+            container['command'] += ['-devNumGuardians', str(num_guardians)]
+
+    return encode_yaml_stream(bridge_yaml)
+
+k8s_yaml(build_bridge_yaml())
 
 k8s_resource("guardian", resource_deps=["proto-gen"])
 
@@ -60,7 +77,6 @@ docker_build(
     live_update = [
         sync("./ethereum/src", "/home/node/app/src"),
     ],
-
 )
 
 k8s_yaml("devnet/eth-devnet.yaml")

+ 0 - 30
bridge/cmd/guardiand/ethlockups.go

@@ -1,30 +0,0 @@
-package main
-
-import (
-	"context"
-	"crypto/ecdsa"
-	"encoding/hex"
-
-	"go.uber.org/zap"
-
-	"github.com/certusone/wormhole/bridge/pkg/common"
-	"github.com/certusone/wormhole/bridge/pkg/supervisor"
-)
-
-func ethLockupProcessor(ec chan *common.ChainLock, gk *ecdsa.PrivateKey) func(ctx context.Context) error {
-	return func(ctx context.Context) error {
-		for {
-			select {
-			case <-ctx.Done():
-				return ctx.Err()
-			case k := <-ec:
-				supervisor.Logger(ctx).Info("lockup confirmed",
-					zap.String("source", hex.EncodeToString(k.SourceAddress[:])),
-					zap.String("target", hex.EncodeToString(k.TargetAddress[:])),
-					zap.String("amount", k.Amount.String()),
-					zap.String("hash", hex.EncodeToString(k.Hash())),
-				)
-			}
-		}
-	}
-}

+ 171 - 0
bridge/cmd/guardiand/ethwatch.go

@@ -0,0 +1,171 @@
+package main
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"crypto/rand"
+	"encoding/hex"
+	"fmt"
+	"time"
+
+	ethcommon "github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/crypto"
+	"go.uber.org/zap"
+	"google.golang.org/protobuf/proto"
+
+	"github.com/certusone/wormhole/bridge/pkg/common"
+	"github.com/certusone/wormhole/bridge/pkg/devnet"
+	gossipv1 "github.com/certusone/wormhole/bridge/pkg/proto/gossip/v1"
+	"github.com/certusone/wormhole/bridge/pkg/supervisor"
+)
+
+// aggregationState represents a single node's aggregation of guardian signatures.
+type (
+	lockupState struct {
+		firstObserved time.Time
+		signatures    map[ethcommon.Address][]byte
+	}
+
+	lockupMap map[string]*lockupState
+
+	aggregationState struct {
+		lockupSignatures lockupMap
+	}
+)
+
+func ethLockupProcessor(lockC chan *common.ChainLock, setC chan *common.GuardianSet, gk *ecdsa.PrivateKey, sendC chan []byte, obsvC chan *gossipv1.EthLockupObservation) func(ctx context.Context) error {
+	return func(ctx context.Context) error {
+		logger := supervisor.Logger(ctx)
+		our_addr := crypto.PubkeyToAddress(gk.PublicKey)
+		state := &aggregationState{lockupMap{}}
+
+		// Get initial validator set
+		logger.Info("waiting for initial validator set to be fetched from Ethereum")
+		gs := <-setC
+		logger.Info("current guardian set received",
+			zap.Strings("set", gs.KeysAsHexStrings()),
+			zap.Uint32("index", gs.Index))
+
+		if *unsafeDevMode {
+			idx, err := devnet.GetDevnetIndex()
+			if err != nil {
+				return err
+			}
+
+			if idx == 0 && (uint(len(gs.Keys)) != *devNumGuardians) {
+				vaa := devnet.DevnetGuardianSetVSS(*devNumGuardians)
+
+				logger.Info(fmt.Sprintf("guardian set has %d members, expecting %d - submitting VAA",
+					len(gs.Keys), *devNumGuardians),
+					zap.Any("vaa", vaa))
+
+				timeout, _ := context.WithTimeout(ctx, 15*time.Second)
+				tx, err := devnet.SubmitVAA(timeout, *ethRPC, vaa)
+				if err != nil {
+					logger.Error("failed to submit devnet guardian set change", zap.Error(err))
+				}
+
+				logger.Info("devnet guardian set change submitted", zap.Any("tx", tx))
+			}
+		}
+
+		for {
+			select {
+			case <-ctx.Done():
+				return ctx.Err()
+			case gs = <-setC:
+				logger.Info("guardian set updated",
+					zap.Strings("set", gs.KeysAsHexStrings()),
+					zap.Uint32("index", gs.Index))
+			case k := <-lockC:
+				supervisor.Logger(ctx).Info("lockup confirmed",
+					zap.String("source", hex.EncodeToString(k.SourceAddress[:])),
+					zap.String("target", hex.EncodeToString(k.TargetAddress[:])),
+					zap.String("amount", k.Amount.String()),
+					zap.String("txhash", k.TxHash.String()),
+					zap.String("lockhash", hex.EncodeToString(k.Hash())),
+				)
+
+				us, ok := gs.KeyIndex(our_addr)
+				if !ok {
+					logger.Error("we're not in the guardian set - refusing to sign",
+						zap.Uint32("index", gs.Index),
+						zap.String("our_addr", our_addr.Hex()),
+						zap.Any("set", gs.KeysAsHexStrings()))
+					break
+				}
+
+				s, err := gk.Sign(rand.Reader, k.Hash(), nil)
+				if err != nil {
+					panic(err)
+				}
+
+				logger.Info("observed and signed confirmed lockup on Ethereum",
+					zap.String("txhash", k.TxHash.String()),
+					zap.String("lockhash", hex.EncodeToString(k.Hash())),
+					zap.String("signature", hex.EncodeToString(s)),
+					zap.Int("us", us))
+
+				obsv := gossipv1.EthLockupObservation{
+					Addr:      crypto.PubkeyToAddress(gk.PublicKey).Bytes(),
+					Hash:      k.Hash(),
+					Signature: s,
+				}
+
+				w := gossipv1.GossipMessage{Message: &gossipv1.GossipMessage_EthLockupObservation{EthLockupObservation: &obsv}}
+
+				msg, err := proto.Marshal(&w)
+				if err != nil {
+					panic(err)
+				}
+
+				sendC <- msg
+			case m := <-obsvC:
+				logger.Info("received another guardian's eth lockup observation",
+					zap.String("hash", hex.EncodeToString(m.Hash)),
+					zap.Binary("signature", m.Signature),
+					zap.Binary("addr", m.Addr))
+
+				their_addr := ethcommon.BytesToAddress(m.Addr)
+				_, ok := gs.KeyIndex(their_addr)
+				if !ok {
+					logger.Warn("received eth observation by unknown guardian - is our guardian set outdated?",
+						zap.String("their_addr", their_addr.Hex()),
+						zap.Any("current_set", gs.KeysAsHexStrings()),
+					)
+					break
+				}
+
+				// TODO: timeout/garbage collection for lockup state
+
+				// []byte isn't hashable in a map. Paying a small extra cost to for encoding for easier debugging.
+				hash := hex.EncodeToString(m.Hash)
+
+				if state.lockupSignatures[hash] == nil {
+					state.lockupSignatures[hash] = &lockupState{
+						firstObserved: time.Now(),
+						signatures:    map[ethcommon.Address][]byte{},
+					}
+				}
+
+				state.lockupSignatures[hash].signatures[their_addr] = m.Signature
+
+				// Enumerate guardian set and check for signatures
+				agg := make([]bool, len(gs.Keys))
+				for i, a := range gs.Keys {
+					// TODO: verify signature
+					_, ok := state.lockupSignatures[hash].signatures[a]
+					agg[i] = ok
+				}
+
+				logger.Info("aggregation state for eth lockup",
+					zap.String("hash", hash),
+					zap.Any("set", gs.KeysAsHexStrings()),
+					zap.Uint32("index", gs.Index),
+					zap.Bools("aggregation", agg))
+
+				// TODO: submit to Solana
+			}
+		}
+	}
+}

+ 0 - 49
bridge/cmd/guardiand/guardiankey.go

@@ -1,49 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"encoding/binary"
-	"fmt"
-	"os"
-	"strconv"
-	"strings"
-)
-
-// getDevnetIndex returns the current host's devnet index (i.e. 0 for guardian-0).
-func getDevnetIndex() (int, error) {
-	hostname, err := os.Hostname()
-	if err != nil {
-		panic(err)
-	}
-
-	h := strings.Split(hostname, "-")
-
-	if h[0] != "guardian" {
-		return 0, fmt.Errorf("hostname %s does not appear to be a devnet host", hostname)
-	}
-
-	i, err := strconv.Atoi(h[1])
-	if err != nil {
-		return 0, fmt.Errorf("invalid devnet index %s in hostname %s", h[1], hostname)
-	}
-
-	return i, nil
-}
-
-// deterministicKeyByIndex generates a deterministic address from a given index.
-func deterministicKeyByIndex(c elliptic.Curve, idx uint64) (*ecdsa.PrivateKey) {
-	buf := make([]byte, 200)
-	binary.LittleEndian.PutUint64(buf, idx)
-
-	worstRNG := bytes.NewBuffer(buf)
-
-	key, err := ecdsa.GenerateKey(c, bytes.NewReader(worstRNG.Bytes()))
-	if err != nil {
-		panic(err)
-	}
-
-	return key
-}
-

+ 77 - 19
bridge/cmd/guardiand/main.go

@@ -5,14 +5,19 @@ import (
 	"crypto/ecdsa"
 	"flag"
 	"fmt"
+	"net/http"
+	_ "net/http/pprof"
 	"os"
 
 	eth_common "github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/libp2p/go-libp2p-core/peer"
 	"go.uber.org/zap"
 
 	"github.com/certusone/wormhole/bridge/pkg/common"
+	"github.com/certusone/wormhole/bridge/pkg/devnet"
 	"github.com/certusone/wormhole/bridge/pkg/ethereum"
+	gossipv1 "github.com/certusone/wormhole/bridge/pkg/proto/gossip/v1"
 	"github.com/certusone/wormhole/bridge/pkg/supervisor"
 
 	ipfslog "github.com/ipfs/go-log/v2"
@@ -29,11 +34,12 @@ var (
 	ethContract      = flag.String("ethContract", "", "Ethereum bridge contract address")
 	ethConfirmations = flag.Uint64("ethConfirmations", 15, "Ethereum confirmation count requirement")
 
-	logLevel = flag.String("loglevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)")
+	logLevel = flag.String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)")
 
 	unsafeDevMode = flag.Bool("unsafeDevMode", false, "Launch node in unsafe, deterministic devnet mode")
+	devNumGuardians = flag.Uint("devNumGuardians", 5, "Number of devnet guardians to include in guardian set")
 
-	nodeName = flag.String("nodeName", "", "Node name to announce in gossip heartbeats (default: hostname)")
+	nodeName = flag.String("nodeName", "", "Node name to announce in gossip heartbeats")
 )
 
 var (
@@ -41,6 +47,22 @@ var (
 	rootCtxCancel context.CancelFunc
 )
 
+// TODO: prometheus metrics
+// TODO: telemetry?
+
+// "Why would anyone do this?" are famous last words.
+//
+// We already forcibly override RPC URLs and keys in dev mode to prevent security
+// risks from operator error, but an extra warning won't hurt.
+const devwarning = `
+        +++++++++++++++++++++++++++++++++++++++++++++++++++
+        |   NODE IS RUNNING IN INSECURE DEVELOPMENT MODE  |
+        |                                                 |
+        |      Do not use -unsafeDevMode in prod.         |
+        +++++++++++++++++++++++++++++++++++++++++++++++++++
+
+`
+
 func rootLoggerName() string {
 	if *unsafeDevMode {
 		// FIXME: add hostname to root logger for cleaner console output in multi-node development.
@@ -61,13 +83,13 @@ func loadGuardianKey(logger *zap.Logger) *ecdsa.PrivateKey {
 
 	if *unsafeDevMode {
 		// Figure out our devnet index
-		idx, err := getDevnetIndex()
+		idx, err := devnet.GetDevnetIndex()
 		if err != nil {
 			logger.Fatal("Failed to parse hostname - are we running in devnet?")
 		}
 
 		// Generate guardian key
-		gk = deterministicKeyByIndex(crypto.S256(), uint64(idx))
+		gk = devnet.DeterministicEcdsaKeyByIndex(crypto.S256(), uint64(idx))
 	} else {
 		panic("not implemented") // TODO
 	}
@@ -81,6 +103,10 @@ func loadGuardianKey(logger *zap.Logger) *ecdsa.PrivateKey {
 func main() {
 	flag.Parse()
 
+	if *unsafeDevMode {
+		fmt.Print(devwarning)
+	}
+
 	// Set up logging. The go-log zap wrapper that libp2p uses is compatible with our
 	// usage of zap in supervisor, which is nice.
 	lvl, err := ipfslog.LevelFromString(*logLevel)
@@ -95,24 +121,46 @@ func main() {
 	// Override the default go-log config, which uses a magic environment variable.
 	ipfslog.SetAllLoggers(lvl)
 
-	// Mute chatty subsystems.
-	if err := ipfslog.SetLogLevel("swarm2", "error"); err != nil {
-		panic(err)
-	} // connection errors
+	// In devnet mode, we automatically set a number of flags that rely on deterministic keys.
+	if *unsafeDevMode {
+		go func() {
+			logger.Info("debug server listening on [::]:6060")
+			logger.Error("debug server crashed", zap.Error(http.ListenAndServe("[::]:6060", nil)))
+		}()
+
+		g0key, err := peer.IDFromPrivateKey(devnet.DeterministicP2PPrivKeyByIndex(0))
+		if err != nil {
+			panic(err)
+		}
+
+		// Use the first guardian node as bootstrap
+		*p2pBootstrap = fmt.Sprintf("/dns4/guardian-0.guardian/udp/%d/quic/p2p/%s", *p2pPort, g0key.String())
+
+		// Deterministic ganache ETH devnet address.
+		*ethContract = devnet.BridgeContractAddress.Hex()
+
+		// Use the hostname as nodeName. For production, we don't want to do this to
+		// prevent accidentally leaking sensitive hostnames.
+		hostname, err := os.Hostname()
+		if err != nil {
+			panic(err)
+		}
+		*nodeName = hostname
+	}
 
 	// Verify flags
-	if *nodeKeyPath == "" {
+
+	if *nodeKeyPath == "" && !*unsafeDevMode { // In devnet mode, keys are deterministically generated.
 		logger.Fatal("Please specify -nodeKey")
 	}
 	if *ethRPC == "" {
 		logger.Fatal("Please specify -ethRPC")
 	}
+	if *ethContract == "" {
+		logger.Fatal("Please specify -ethContract")
+	}
 	if *nodeName == "" {
-		hostname, err := os.Hostname()
-		if err != nil {
-			panic(err)
-		}
-		*nodeName = hostname
+		logger.Fatal("Please specify -nodeName")
 	}
 
 	ethContractAddr := eth_common.HexToAddress(*ethContract)
@@ -125,20 +173,31 @@ func main() {
 	defer rootCtxCancel()
 
 	// Ethereum lock event channel
-	ec := make(chan *common.ChainLock)
+	lockC := make(chan *common.ChainLock)
+
+	// Ethereum incoming guardian set updates
+	setC := make(chan *common.GuardianSet)
+
+	// Outbound gossip message queue
+	sendC := make(chan []byte)
+
+	// Inbound ETH observations
+	ethObsvC := make(chan *gossipv1.EthLockupObservation)
 
 	// Run supervisor.
 	supervisor.New(rootCtx, logger, func(ctx context.Context) error {
-		if err := supervisor.Run(ctx, "p2p", p2p); err != nil {
+		// TODO: use a dependency injection framework like wire?
+
+		if err := supervisor.Run(ctx, "p2p", p2p(ethObsvC, sendC)); err != nil {
 			return err
 		}
 
 		if err := supervisor.Run(ctx, "eth",
-			ethereum.NewEthBridgeWatcher(*ethRPC, ethContractAddr, *ethConfirmations, ec).Run); err != nil {
+			ethereum.NewEthBridgeWatcher(*ethRPC, ethContractAddr, *ethConfirmations, lockC, setC).Run); err != nil {
 			return err
 		}
 
-		if err := supervisor.Run(ctx, "lockups", ethLockupProcessor(ec, gk)); err != nil {
+		if err := supervisor.Run(ctx, "ethwatch", ethLockupProcessor(lockC, setC, gk, sendC, ethObsvC)); err != nil {
 			return err
 		}
 
@@ -157,4 +216,3 @@ func main() {
 		// TODO: wait for things to shut down gracefully
 	}
 }
-

+ 0 - 26
bridge/cmd/guardiand/nodekey.go

@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/base64"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -47,28 +46,3 @@ func getOrCreateNodeKey(logger *zap.Logger, path string) (crypto.PrivKey, error)
 
 	return priv, nil
 }
-
-// deterministicNodeKey returns a non-nil value if we have a deterministic key on file for the current host.
-func deterministicNodeKey() crypto.PrivKey {
-	idx, err := getDevnetIndex()
-	if err != nil {
-		panic(err)
-	}
-
-	if idx == 0 {
-		// node ID: 12D3KooWQ1sV2kowPY1iJX1hJcVTysZjKv3sfULTGwhdpUGGZ1VF
-		b, err := base64.StdEncoding.DecodeString("CAESQGlv6OJOMXrZZVTCC0cgCv7goXr6QaSVMZIndOIXKNh80vYnG+EutVlZK20Nx9cLkUG5ymKB\n88LXi/vPBwP8zfY=")
-		if err != nil {
-			panic(err)
-		}
-
-		priv, err := crypto.UnmarshalPrivateKey(b)
-		if err != nil {
-			panic(err)
-		}
-
-		return priv
-	}
-
-	return nil
-}

+ 185 - 137
bridge/cmd/guardiand/p2p.go

@@ -16,189 +16,237 @@ import (
 	dht "github.com/libp2p/go-libp2p-kad-dht"
 	pubsub "github.com/libp2p/go-libp2p-pubsub"
 	libp2pquic "github.com/libp2p/go-libp2p-quic-transport"
-	swarm "github.com/libp2p/go-libp2p-swarm"
 	libp2ptls "github.com/libp2p/go-libp2p-tls"
 	"github.com/multiformats/go-multiaddr"
 	"go.uber.org/zap"
 	"google.golang.org/protobuf/proto"
 
+	"github.com/certusone/wormhole/bridge/pkg/devnet"
 	gossipv1 "github.com/certusone/wormhole/bridge/pkg/proto/gossip/v1"
 	"github.com/certusone/wormhole/bridge/pkg/supervisor"
 )
 
-func p2p(ctx context.Context) (re error) {
-	logger := supervisor.Logger(ctx)
-
-	var priv crypto.PrivKey
-	var err error
-
-	if *unsafeDevMode {
-		priv = deterministicNodeKey()
+func p2p(ethObsvC chan *gossipv1.EthLockupObservation, sendC chan []byte) func(ctx context.Context) error {
+	return func(ctx context.Context) (re error) {
+		logger := supervisor.Logger(ctx)
 
+		var priv crypto.PrivKey
 		var err error
-		if priv == nil {
+
+		if *unsafeDevMode {
+			idx, err2 := devnet.GetDevnetIndex()
+			if err2 != nil {
+				logger.Fatal("Failed to parse hostname - are we running in devnet?")
+			}
+			priv = devnet.DeterministicP2PPrivKeyByIndex(int64(idx))
+		} else {
 			priv, err = getOrCreateNodeKey(logger, *nodeKeyPath)
 			if err != nil {
 				return fmt.Errorf("failed to load node key: %w", err)
 			}
-		} else {
-			logger.Info("devnet: loaded hardcoded node key")
 		}
-	} else {
-		priv, err = getOrCreateNodeKey(logger, *nodeKeyPath)
+
+		var idht *dht.IpfsDHT
+
+		h, err := libp2p.New(ctx,
+			// Use the keypair we generated
+			libp2p.Identity(priv),
+
+			// Multiple listen addresses
+			libp2p.ListenAddrStrings(
+				// Listen on QUIC only.
+				// TODO(leo): is this more or less stable than using both TCP and QUIC transports?
+				// https://github.com/libp2p/go-libp2p/issues/688
+				fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic", *p2pPort),
+				fmt.Sprintf("/ip6/::/udp/%d/quic", *p2pPort),
+			),
+
+			// Enable TLS security as the only security protocol.
+			libp2p.Security(libp2ptls.ID, libp2ptls.New),
+
+			// Enable QUIC transport as the only transport.
+			libp2p.Transport(libp2pquic.NewTransport),
+
+			// Let's prevent our peer from having too many
+			// connections by attaching a connection manager.
+			libp2p.ConnectionManager(connmgr.NewConnManager(
+				100,         // Lowwater
+				400,         // HighWater,
+				time.Minute, // GracePeriod
+			)),
+
+			// Let this host use the DHT to find other hosts
+			libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) {
+				// TODO(leo): Persistent data store (i.e. address book)
+				idht, err = dht.New(ctx, h, dht.Mode(dht.ModeServer),
+					// TODO(leo): This intentionally makes us incompatible with the global IPFS DHT
+					dht.ProtocolPrefix(protocol.ID("/"+*p2pNetworkID)),
+				)
+				return idht, err
+			}),
+		)
+
 		if err != nil {
-			return fmt.Errorf("failed to load node key: %w", err)
+			panic(err)
 		}
-	}
 
-	var idht *dht.IpfsDHT
-
-	h, err := libp2p.New(ctx,
-		// Use the keypair we generated
-		libp2p.Identity(priv),
-		// Multiple listen addresses
-		libp2p.ListenAddrStrings(
-			// Listen on QUIC only.
-			// TODO(leo): listen on ipv6
-			// TODO(leo): is this more or less stable than using both TCP and QUIC transports?
-			// https://github.com/libp2p/go-libp2p/issues/688
-			fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic", *p2pPort),
-		),
-
-		// Enable TLS security only.
-		libp2p.Security(libp2ptls.ID, libp2ptls.New),
-
-		// Enable QUIC transports.
-		libp2p.Transport(libp2pquic.NewTransport),
-
-		// Enable TCP so we can connect to bootstrap nodes.
-		// (can be disabled if we bootstrap our own network)
-		libp2p.DefaultTransports,
-
-		// Let's prevent our peer from having too many
-		// connections by attaching a connection manager.
-		libp2p.ConnectionManager(connmgr.NewConnManager(
-			100,         // Lowwater
-			400,         // HighWater,
-			time.Minute, // GracePeriod
-		)),
-
-		// Let this host use the DHT to find other hosts
-		libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) {
-			// TODO(leo): Persistent data store (i.e. address book)
-			idht, err = dht.New(ctx, h, dht.Mode(dht.ModeServer),
-				// TODO(leo): This intentionally makes us incompatible with the global IPFS DHT
-				dht.ProtocolPrefix(protocol.ID("/"+*p2pNetworkID)),
-			)
-			return idht, err
-		}),
-	)
-
-	if err != nil {
-		panic(err)
-	}
+		defer func() {
+			// TODO: libp2p cannot be cleanly restarted (https://github.com/libp2p/go-libp2p/issues/992)
+			logger.Error("p2p routine has exited, cancelling root context...", zap.Error(re))
+			rootCtxCancel()
+		}()
 
-	defer func() {
-		// TODO: libp2p cannot be cleanly restarted (https://github.com/libp2p/go-libp2p/issues/992)
-		logger.Error("p2p routine has exited, cancelling root context...", zap.Error(re))
-		rootCtxCancel()
-	}()
+		logger.Info("Connecting to bootstrap peers", zap.String("bootstrap_peers", *p2pBootstrap))
 
-	logger.Info("Connecting to bootstrap peers")
+		// Add our own bootstrap nodes
 
-	// Add our own bootstrap nodes
+		// Count number of successful connection attempts. If we fail to connect to every bootstrap peer, kill
+		// the service and have supervisor retry it.
+		successes := 0
+		// Are we a bootstrap node? If so, it's okay to not have any peers.
+		bootstrap_node := false
 
-	// Count number of successful connection attempts. If we fail to connect to every bootstrap peer, kill
-	// the service and have supervisor retry it.
-	successes := 0
+		for _, addr := range strings.Split(*p2pBootstrap, ",") {
+			if addr == "" {
+				continue
+			}
+			ma, err := multiaddr.NewMultiaddr(addr)
+			if err != nil {
+				logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err))
+				continue
+			}
+			pi, err := peer.AddrInfoFromP2pAddr(ma)
+			if err != nil {
+				logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err))
+				continue
+			}
 
-	for _, addr := range strings.Split(*p2pBootstrap, ",") {
-		if addr == "" {
-			continue
-		}
-		ma, err := multiaddr.NewMultiaddr(addr)
-		if err != nil {
-			logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err))
-			continue
-		}
-		pi, err := peer.AddrInfoFromP2pAddr(ma)
-		if err != nil {
-			logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err))
-			continue
-		}
+			if pi.ID == h.ID() {
+				logger.Info("We're a bootstrap node")
+				bootstrap_node = true
+				continue
+			}
 
-		if err = h.Connect(ctx, *pi); err != nil {
-			if err != swarm.ErrDialToSelf {
+			if err = h.Connect(ctx, *pi); err != nil {
 				logger.Error("Failed to connect to bootstrap peer", zap.String("peer", addr), zap.Error(err))
 			} else {
-				// Dialing self, carrying on... (we're a bootstrap peer)
-				logger.Info("Tried to connect to ourselves - we're a bootstrap peer")
 				successes += 1
 			}
-		} else {
-			successes += 1
 		}
-	}
 
-	if successes == 0 {
-		h.Close()
-		return fmt.Errorf("Failed to connect to any bootstrap peer")
-	} else {
-		logger.Info("Connected to bootstrap peers", zap.Int("num", successes))
-	}
+		// TODO: continually reconnect to bootstrap nodes?
+		if successes == 0 && !bootstrap_node {
+			return fmt.Errorf("Failed to connect to any bootstrap peer")
+		} else {
+			logger.Info("Connected to bootstrap peers", zap.Int("num", successes))
+		}
 
-	topic := fmt.Sprintf("%s/%s", *p2pNetworkID, "broadcast")
+		topic := fmt.Sprintf("%s/%s", *p2pNetworkID, "broadcast")
 
-	logger.Info("Subscribing pubsub topic", zap.String("topic", topic))
-	ps, err := pubsub.NewGossipSub(ctx, h)
-	if err != nil {
-		panic(err)
-	}
+		logger.Info("Subscribing pubsub topic", zap.String("topic", topic))
+		ps, err := pubsub.NewGossipSub(ctx, h)
+		if err != nil {
+			panic(err)
+		}
 
-	th, err := ps.Join(topic)
-	if err != nil {
-		return fmt.Errorf("failed to join topic: %w", err)
-	}
+		th, err := ps.Join(topic)
+		if err != nil {
+			return fmt.Errorf("failed to join topic: %w", err)
+		}
 
-	sub, err := th.Subscribe()
-	if err != nil {
-		return fmt.Errorf("failed to subscribe topic: %w", err)
-	}
+		sub, err := th.Subscribe()
+		if err != nil {
+			return fmt.Errorf("failed to subscribe topic: %w", err)
+		}
 
-	logger.Info("Node has been started", zap.String("peer_id", h.ID().String()),
-		zap.String("addrs", fmt.Sprintf("%v", h.Addrs())))
+		logger.Info("Node has been started", zap.String("peer_id", h.ID().String()),
+			zap.String("addrs", fmt.Sprintf("%v", h.Addrs())))
+
+		go func() {
+			ctr := int64(0)
+			tick := time.NewTicker(15 * time.Second)
+			defer tick.Stop()
+
+			for {
+				select {
+				case <-ctx.Done():
+					return
+				case <-tick.C:
+					msg := gossipv1.GossipMessage{Message: &gossipv1.GossipMessage_Heartbeat{
+						Heartbeat: &gossipv1.Heartbeat{
+							NodeName:  *nodeName,
+							Counter:   ctr,
+							Timestamp: time.Now().UnixNano(),
+						}}}
+
+					b, err := proto.Marshal(&msg)
+					if err != nil {
+						panic(err)
+					}
+
+					err = th.Publish(ctx, b)
+					if err != nil {
+						logger.Warn("failed to publish heartbeat message", zap.Error(err))
+					}
+				}
+			}
+		}()
+
+		go func() {
+			for {
+				select {
+				case <-ctx.Done():
+					return
+				case msg := <-sendC:
+					err := th.Publish(ctx, msg)
+					if err != nil {
+						logger.Error("failed to publish message from queue", zap.Error(err))
+					}
+				}
+			}
+		}()
 
-	go func() {
-		ctr := int64(0)
+		supervisor.Signal(ctx, supervisor.SignalHealthy)
 
 		for {
-			msg := gossipv1.Heartbeat{
-				NodeName: *nodeName,
-				Counter:  ctr,
-			}
-
-			b, err := proto.Marshal(&msg)
+			envl, err := sub.Next(ctx)
 			if err != nil {
-				panic(err)
+				return fmt.Errorf("failed to receive pubsub message: %w", err)
 			}
 
-			err = th.Publish(ctx, b)
+			var msg gossipv1.GossipMessage
+			err = proto.Unmarshal(envl.Data, &msg)
 			if err != nil {
-				logger.Warn("failed to publish message", zap.Error(err))
+				logger.Info("received invalid message",
+					zap.String("data", string(envl.Data)),
+					zap.String("from", envl.GetFrom().String()))
+				continue
 			}
 
-			time.Sleep(15 * time.Second)
-		}
-	}()
-
-	supervisor.Signal(ctx, supervisor.SignalHealthy)
+			if envl.GetFrom() == h.ID() {
+				logger.Debug("received message from ourselves, ignoring",
+					zap.Any("payload", msg.Message))
+				continue
+			}
 
-	for {
-		msg, err := sub.Next(ctx)
-		if err != nil {
-			return fmt.Errorf("failed to receive pubsub message: %w", err)
+			logger.Debug("received message",
+				zap.Any("payload", msg.Message),
+				zap.Binary("raw", envl.Data),
+				zap.String("from", envl.GetFrom().String()))
+
+			switch m := msg.Message.(type) {
+			case *gossipv1.GossipMessage_Heartbeat:
+				logger.Info("heartbeat received",
+					zap.Any("value", m.Heartbeat),
+					zap.String("from", envl.GetFrom().String()))
+			case *gossipv1.GossipMessage_EthLockupObservation:
+				ethObsvC <- m.EthLockupObservation
+			default:
+				logger.Warn("received unknown message type (running outdated software?)",
+					zap.Any("payload", msg.Message),
+					zap.Binary("raw", envl.Data),
+					zap.String("from", envl.GetFrom().String()))
+			}
 		}
-
-		logger.Debug("received message", zap.String("data", string(msg.Data)), zap.String("from", msg.GetFrom().String()))
 	}
 }

+ 13 - 29
bridge/cmd/vaa-test/main.go

@@ -3,17 +3,19 @@ package main
 import (
 	"crypto/ecdsa"
 	"encoding/hex"
-	"github.com/certusone/wormhole/bridge/pkg/vaa"
-	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/crypto"
 	"math/big"
 	"math/rand"
 	"time"
-)
 
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/crypto"
+
+	"github.com/certusone/wormhole/bridge/pkg/devnet"
+	"github.com/certusone/wormhole/bridge/pkg/vaa"
+)
 
 func main() {
-	addr := common.HexToAddress("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")
+	addr := devnet.GanacheClientDefaultAccountAddress
 	addrP := common.LeftPadBytes(addr[:], 32)
 	addrTarget := vaa.Address{}
 	copy(addrTarget[:], addrP)
@@ -83,11 +85,11 @@ func main() {
 	//	},
 	//}
 
-	AddSignature(v,key,0)
-	AddSignature(v,key2,1)
-	AddSignature(v,key3,2)
-	AddSignature(v,key5,4)
-	AddSignature(v,key6,5)
+	v.AddSignature(key, 0)
+	v.AddSignature(key2, 1)
+	v.AddSignature(key3, 2)
+	v.AddSignature(key5, 4)
+	v.AddSignature(key6, 5)
 	sigAddr := crypto.PubkeyToAddress(key.PublicKey)
 	println(sigAddr.String())
 	println(crypto.PubkeyToAddress(key2.PublicKey).String())
@@ -96,28 +98,10 @@ func main() {
 	println(crypto.PubkeyToAddress(key5.PublicKey).String())
 	println(crypto.PubkeyToAddress(key6.PublicKey).String())
 
-	vData, err := v.Serialize()
+	vData, err := v.Marshal()
 	if err != nil {
 		panic(err)
 	}
 
 	println(hex.EncodeToString(vData))
 }
-
-func AddSignature(v *vaa.VAA, key *ecdsa.PrivateKey,index uint8){
-	data, err := v.SigningMsg()
-	if err != nil {
-		panic(err)
-	}
-	sig, err := crypto.Sign(data.Bytes(), key)
-	if err != nil {
-		panic(err)
-	}
-	sigData := [65]byte{}
-	copy(sigData[:], sig)
-
-	v.Signatures = append(v.Signatures, &vaa.Signature{
-		Index:     index,
-		Signature: sigData,
-	})
-}

+ 1 - 6
bridge/go.mod

@@ -3,13 +3,11 @@ module github.com/certusone/wormhole/bridge
 go 1.14
 
 require (
-	github.com/aristanetworks/goarista v0.0.0-20190204200901-2166578f3448 // indirect
 	github.com/cenkalti/backoff/v4 v4.0.2
 	github.com/cespare/cp v1.1.1 // indirect
 	github.com/deckarep/golang-set v1.7.1 // indirect
 	github.com/ethereum/go-ethereum v1.9.18
 	github.com/go-kit/kit v0.9.0 // indirect
-	github.com/go-logfmt/logfmt v0.4.0 // indirect
 	github.com/golang/mock v1.4.3 // indirect
 	github.com/golang/protobuf v1.4.2
 	github.com/ipfs/go-log/v2 v2.1.1
@@ -19,17 +17,14 @@ require (
 	github.com/libp2p/go-libp2p-kad-dht v0.8.3
 	github.com/libp2p/go-libp2p-pubsub v0.3.3
 	github.com/libp2p/go-libp2p-quic-transport v0.7.1
-	github.com/libp2p/go-libp2p-swarm v0.2.8
 	github.com/libp2p/go-libp2p-tls v0.1.3
 	github.com/mattn/go-colorable v0.1.4 // indirect
 	github.com/mattn/go-isatty v0.0.12 // indirect
+	github.com/miguelmota/go-ethereum-hdwallet v0.0.0-20200123000308-a60dcd172b4c
 	github.com/multiformats/go-multiaddr v0.2.2
 	github.com/olekukonko/tablewriter v0.0.4 // indirect
 	github.com/onsi/gomega v1.10.1 // indirect
-	github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect
 	github.com/prometheus/tsdb v0.7.1 // indirect
-	github.com/rjeczalik/notify v0.9.2 // indirect
-	github.com/rs/cors v1.6.0 // indirect
 	github.com/stretchr/testify v1.6.1
 	go.uber.org/zap v1.15.0
 	golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 // indirect

+ 87 - 6
bridge/go.sum

@@ -24,8 +24,11 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
 github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
 github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 github.com/VictoriaMetrics/fastcache v1.5.7 h1:4y6y0G8PRzszQUYIQHHssv/jgPHAb5qQuuDNdCbyAgw=
@@ -35,16 +38,23 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
+github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
+github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/aristanetworks/fsnotify v1.4.2/go.mod h1:D/rtu7LpjYM8tRJphJ0hUBYpjai8SfX+aSNsWDTq/Ks=
+github.com/aristanetworks/glog v0.0.0-20180419172825-c15b03b3054f/go.mod h1:KASm+qXFKs/xjSoWn30NrWBBvdTTQq+UjkhjEJHfSFA=
 github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
-github.com/aristanetworks/goarista v0.0.0-20190204200901-2166578f3448 h1:c7dHl/Dp2sznWCZm0FCiQEJEoxEbTAZV7Ccdojs7Bwo=
-github.com/aristanetworks/goarista v0.0.0-20190204200901-2166578f3448/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
+github.com/aristanetworks/goarista v0.0.0-20190912214011-b54698eaaca6 h1:6bZNnQcA2fkzH9AhZXbp2nDqbWa4bBqFeUb70Zq1HBM=
+github.com/aristanetworks/goarista v0.0.0-20190912214011-b54698eaaca6/go.mod h1:Z4RTxGAuYhPzcq8+EdRM+R8M48Ssle2TsWtwRKa+vns=
+github.com/aristanetworks/splunk-hec-go v0.3.3/go.mod h1:1VHO9r17b0K7WmOlLb9nTk/2YanvOEnLMUgsFrxBROc=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/benbjohnson/clock v1.0.2/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
 github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg=
 github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
 github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
 github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8=
@@ -54,6 +64,7 @@ github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQj
 github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
 github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
+github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
 github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
 github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
 github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
@@ -107,11 +118,18 @@ github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
 github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/dop251/goja v0.0.0-20200219165308-d1232e640a87/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c h1:JHHhtb9XWJrGNMcrVP6vyzO4dusgi/HnceHTgxSejUM=
 github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elastic/gosigar v0.10.5/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ethereum/go-ethereum v1.9.5/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY=
 github.com/ethereum/go-ethereum v1.9.18 h1:+vzvufVD7+OfQa07IJP20Z7AGZsJaw0M6JIA/WQcqy8=
 github.com/ethereum/go-ethereum v1.9.18/go.mod h1:JSSTypSMTkGZtAdAChH2wP5dZEvPGh3nUTuDpH+hNrg=
 github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -122,6 +140,7 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn
 github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
 github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -178,12 +197,14 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
 github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY=
 github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@@ -202,8 +223,10 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
 github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
@@ -215,6 +238,7 @@ github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7
 github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
+github.com/influxdata/influxdb1-client v0.0.0-20190809212627-fc22c7df067e/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
 github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
 github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
@@ -272,21 +296,27 @@ github.com/jbenet/goprocess v0.0.0-20160826012719-b497e2f366b8/go.mod h1:Ly/wlsj
 github.com/jbenet/goprocess v0.1.3/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
 github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
+github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
 github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
+github.com/karalabe/hid v1.0.0/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8=
 github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356 h1:I/yrLt2WilKxlQKCM52clh5rGzTKpVctGT1lH4Dc8Jw=
 github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
+github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d h1:68u9r4wEvL3gYg2jvAOgROwZ3H+Y3hIDk4tbbmIjcYQ=
 github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
@@ -520,6 +550,8 @@ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00v
 github.com/miekg/dns v1.1.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.28/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
 github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/miguelmota/go-ethereum-hdwallet v0.0.0-20200123000308-a60dcd172b4c h1:cbhK2JT4nl7k8frmCN98ttRdSGP75x9mDxDhlQ1kHQQ=
+github.com/miguelmota/go-ethereum-hdwallet v0.0.0-20200123000308-a60dcd172b4c/go.mod h1:Z4zI+CdJB1fyrZ1jfevFH6flNV9izrLZnQAeuD6Wkjk=
 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=
 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
 github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
@@ -530,7 +562,9 @@ github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKU
 github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
 github.com/mr-tron/base58 v1.1.1/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
@@ -586,6 +620,7 @@ github.com/multiformats/go-varint v0.0.2/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXS
 github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
 github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
 github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
 github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
@@ -600,55 +635,70 @@ github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FW
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
 github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/openconfig/gnmi v0.0.0-20190823184014-89b2bf29312c/go.mod h1:t+O9It+LKzfOAhKTT5O0ehDix+MTqbtT0T9t+7zzOvc=
+github.com/openconfig/reference v0.0.0-20190727015836-8dfd928c9696/go.mod h1:ym2A+zigScwkSEb/cVQB0/ZMpU3rqiH6X7WRRsxgOGw=
 github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
-github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 h1:zNBQb37RGLmJybyMcs983HfUfpkw9OTFD9tbBfAViHE=
-github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
+github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM=
 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
+github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
 github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8=
 github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
-github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
-github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
+github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shirou/gopsutil v2.20.5+incompatible h1:tYH07UPoQt0OCQdgWWMgYHy3/a9bcxNpBIysykNIP7I=
 github.com/shirou/gopsutil v2.20.5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
@@ -707,10 +757,15 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/syndtr/goleveldb v0.0.0-20180621010148-0d5a0ceb10cf/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs=
 github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
+github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
+github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
+github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
+github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
 github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4=
 github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@@ -731,7 +786,11 @@ github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee/go.mod h1:
 github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 h1:1cngl9mPEoITZG8s8cVcUy5CeIBYhEESkOB7m6Gmkrk=
 github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees=
 github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
+github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
+github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xtaci/kcp-go v5.4.5+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
+github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -764,11 +823,13 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190225124518-7f87c0fbb88b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -791,6 +852,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -798,7 +860,10 @@ golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -823,18 +888,23 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -866,6 +936,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190912185636-87d9f09c5d89/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -900,6 +971,7 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
@@ -912,17 +984,26 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fatih/set.v0 v0.2.1/go.mod h1:5eLWEndGL4zGGemXWrKuts+wTJR0y+w+auqUJZbmyBg=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
+gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
+gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
+gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
+gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
+gopkg.in/karalabe/cookiejar.v2 v2.0.0-20150724131613-8dcd6a7f4951/go.mod h1:owOxCRGGeAx1uugABik6K9oeNu1cgxP/R9ItzLDxNWA=
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6 h1:a6cXbcDDUkSBlpnkWV1bJ+vv3mOgQEltEJ2rPxroVu0=
 gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
+gopkg.in/redis.v4 v4.2.4/go.mod h1:8KREHdypkCEojGKQcjMqAODMICIVwZAONWq8RowTITA=
 gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8=
 gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=

+ 34 - 0
bridge/pkg/common/guardianset.go

@@ -0,0 +1,34 @@
+package common
+
+import (
+	"github.com/ethereum/go-ethereum/common"
+)
+
+type GuardianSet struct {
+	// Guardian's public keys truncated by the ETH standard hashing mechanism (20 bytes).
+	Keys []common.Address
+	// On-chain set index
+	Index uint32
+}
+
+func (g *GuardianSet) KeysAsHexStrings() []string {
+	r := make([]string, len(g.Keys))
+
+	for n, k := range g.Keys {
+		r[n] = k.Hex()
+	}
+
+	return r
+}
+
+// Get a given address index from the guardian set. Returns (-1, false)
+// if the address wasn't found and (addr, true) otherwise.
+func (g *GuardianSet) KeyIndex(addr common.Address) (int, bool) {
+	for n, k := range g.Keys {
+		if k == addr {
+			return n, true
+		}
+	}
+
+	return -1, false
+}

+ 44 - 0
bridge/pkg/devnet/constants.go

@@ -0,0 +1,44 @@
+// package devnet contains constants and helper functions for the local deterministic devnet.
+package devnet
+
+import (
+	"fmt"
+
+	"github.com/ethereum/go-ethereum/accounts"
+	"github.com/ethereum/go-ethereum/common"
+
+	"github.com/miguelmota/go-ethereum-hdwallet"
+)
+
+var (
+	// Address of the first account, which is used as the default client account.
+	GanacheClientDefaultAccountAddress = common.HexToAddress("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")
+
+	// Contracts (deployed by "truffle migrate" on a deterministic devnet)
+	WrappedAssetContractAddress = common.HexToAddress("0x79183957Be84C0F4dA451E534d5bA5BA3FB9c696")
+	BridgeContractAddress       = common.HexToAddress("0xCfEB869F69431e42cdB54A4F4f105C19C080A601")
+)
+
+const (
+	// Ganache's hardcoded HD Wallet derivation path
+	ganacheWalletMnemonic = "myth like bonus scare over problem client lizard pioneer submit female collect"
+	ganacheDerivationPath = "m/44'/60'/0'/0/%d"
+)
+
+func DeriveAccount(accountIndex uint) accounts.Account {
+	path := hdwallet.MustParseDerivationPath(fmt.Sprintf(ganacheDerivationPath, accountIndex))
+	account, err := Wallet().Derive(path, false)
+	if err != nil {
+		panic(err)
+	}
+
+	return account
+}
+
+func Wallet() *hdwallet.Wallet {
+	wallet, err := hdwallet.NewFromMnemonic(ganacheWalletMnemonic)
+	if err != nil {
+		panic(err)
+	}
+	return wallet
+}

+ 37 - 0
bridge/pkg/devnet/deterministic_key.go

@@ -0,0 +1,37 @@
+package devnet
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"encoding/binary"
+	mathrand "math/rand"
+
+	"github.com/libp2p/go-libp2p-core/crypto"
+)
+
+// DeterministicEcdsaKeyByIndex generates a deterministic ecdsa.PrivateKey from a given index.
+func DeterministicEcdsaKeyByIndex(c elliptic.Curve, idx uint64) *ecdsa.PrivateKey {
+	buf := make([]byte, 200)
+	binary.LittleEndian.PutUint64(buf, idx)
+
+	worstRNG := bytes.NewBuffer(buf)
+
+	key, err := ecdsa.GenerateKey(c, bytes.NewReader(worstRNG.Bytes()))
+	if err != nil {
+		panic(err)
+	}
+
+	return key
+}
+
+// DeterministicP2PPrivKeyByIndex generates a deterministic libp2p crypto.PrivateKey from a given index.
+func DeterministicP2PPrivKeyByIndex(idx int64) crypto.PrivKey {
+	r := mathrand.New(mathrand.NewSource(int64(idx)))
+	priv, _, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, -1, r)
+	if err != nil {
+		panic(err)
+	}
+
+	return priv
+}

+ 75 - 0
bridge/pkg/devnet/guardianset_vaa.go

@@ -0,0 +1,75 @@
+package devnet
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"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/ethclient"
+
+	"github.com/certusone/wormhole/bridge/pkg/ethereum/abi"
+	"github.com/certusone/wormhole/bridge/pkg/vaa"
+)
+
+// DevnetGuardianSetVSS returns a VAA signed by guardian-0 that adds all n validators.
+func DevnetGuardianSetVSS(n uint) *vaa.VAA {
+	pubkeys := make([]common.Address, n)
+
+	for n := range pubkeys {
+		key := DeterministicEcdsaKeyByIndex(crypto.S256(), uint64(n))
+		pubkeys[n] = crypto.PubkeyToAddress(key.PublicKey)
+	}
+
+	v := &vaa.VAA{
+		Version:          1,
+		GuardianSetIndex: 0,
+		Timestamp:        time.Unix(5000, 0),
+		Payload: &vaa.BodyGuardianSetUpdate{
+			Keys:     pubkeys,
+			NewIndex: 1,
+		},
+	}
+
+	// The devnet is initialized with a single guardian (ethereum/migrations/1_initial_migration.js).
+	key0 := DeterministicEcdsaKeyByIndex(crypto.S256(), 0)
+	v.AddSignature(key0, 0)
+
+	return v
+}
+
+// SubmitVAA submits a VAA to the devnet chain using well-known accounts and contract addresses.
+func SubmitVAA(ctx context.Context, rpcURL string, vaa *vaa.VAA) (*types.Transaction, error) {
+	c, err := ethclient.DialContext(ctx, rpcURL)
+	if err != nil {
+		return nil, fmt.Errorf("dialing eth client failed: %w", err)
+	}
+
+	key, err := Wallet().PrivateKey(DeriveAccount(0))
+	if err != nil {
+		panic(err)
+	}
+
+	opts := bind.NewKeyedTransactor(key)
+	opts.Context = ctx
+
+	bridge, err := abi.NewAbi(BridgeContractAddress, c)
+	if err != nil {
+		panic(err)
+	}
+
+	b, err := vaa.Marshal()
+	if err != nil {
+		panic(err)
+	}
+
+	tx, err := bridge.SubmitVAA(opts, b)
+	if err != nil {
+		return nil, err
+	}
+
+	return tx, nil
+}

+ 29 - 0
bridge/pkg/devnet/hostname.go

@@ -0,0 +1,29 @@
+package devnet
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+)
+
+// GetDevnetIndex returns the current host's devnet index (i.e. 0 for guardian-0).
+func GetDevnetIndex() (int, error) {
+	hostname, err := os.Hostname()
+	if err != nil {
+		panic(err)
+	}
+
+	h := strings.Split(hostname, "-")
+
+	if h[0] != "guardian" {
+		return 0, fmt.Errorf("hostname %s does not appear to be a devnet host", hostname)
+	}
+
+	i, err := strconv.Atoi(h[1])
+	if err != nil {
+		return 0, fmt.Errorf("invalid devnet index %s in hostname %s", h[1], hostname)
+	}
+
+	return i, nil
+}

+ 72 - 17
bridge/pkg/ethereum/watcher.go

@@ -27,38 +27,55 @@ type (
 		pendingLocks      map[eth_common.Hash]*pendingLock
 		pendingLocksGuard sync.Mutex
 
-		evChan chan *common.ChainLock
+		lockChan chan *common.ChainLock
+		setChan  chan *common.GuardianSet
 	}
 
 	pendingLock struct {
-		lock *common.ChainLock
+		lock   *common.ChainLock
 		height uint64
 	}
 )
 
-func NewEthBridgeWatcher(url string, bridge eth_common.Address, minConfirmations uint64, events chan *common.ChainLock) *EthBridgeWatcher {
-	return &EthBridgeWatcher{url: url, bridge: bridge, minConfirmations: minConfirmations, evChan: events, pendingLocks: map[eth_common.Hash]*pendingLock{}}
+func NewEthBridgeWatcher(url string, bridge eth_common.Address, minConfirmations uint64, lockEvents chan *common.ChainLock, setEvents chan *common.GuardianSet) *EthBridgeWatcher {
+	return &EthBridgeWatcher{url: url, bridge: bridge, minConfirmations: minConfirmations, lockChan: lockEvents, setChan: setEvents, pendingLocks: map[eth_common.Hash]*pendingLock{}}
 }
 
 func (e *EthBridgeWatcher) Run(ctx context.Context) error {
-	c, err := ethclient.DialContext(ctx, e.url)
+	timeout, _ := context.WithTimeout(ctx, 15 * time.Second)
+	c, err := ethclient.DialContext(timeout, e.url)
 	if err != nil {
 		return fmt.Errorf("dialing eth client failed: %w", err)
 	}
 
-	f, err := abi.NewWormholeBridgeFilterer(e.bridge, c)
+	f, err := abi.NewAbiFilterer(e.bridge, c)
 	if err != nil {
 		return fmt.Errorf("could not create wormhole bridge filter: %w", err)
 	}
 
-	sink := make(chan *abi.WormholeBridgeLogTokensLocked, 2)
-	eventSubscription, err := f.WatchLogTokensLocked(&bind.WatchOpts{
-		Context: ctx,
-	}, sink, nil, nil)
+	caller, err := abi.NewAbiCaller(e.bridge, c)
 	if err != nil {
-		return fmt.Errorf("failed to subscribe to eth events: %w", err)
+		panic(err)
 	}
-	defer eventSubscription.Unsubscribe()
+
+	// Timeout for initializing subscriptions
+	timeout, _ = context.WithTimeout(ctx, 15 * time.Second)
+
+	// Subscribe to new token lockups
+	tokensLockedC := make(chan *abi.AbiLogTokensLocked, 2)
+	tokensLockedSub, err := f.WatchLogTokensLocked(&bind.WatchOpts{Context: timeout}, tokensLockedC, nil, nil)
+	if err != nil {
+		return fmt.Errorf("failed to subscribe to token lockup events: %w", err)
+	}
+	defer tokensLockedSub.Unsubscribe()
+
+	// Subscribe to guardian set changes
+	guardianSetC := make(chan *abi.AbiLogGuardianSetChanged, 2)
+	guardianSetEvent, err := f.WatchLogGuardianSetChanged(&bind.WatchOpts{Context: timeout}, guardianSetC)
+	if err != nil {
+		return fmt.Errorf("failed to subscribe to guardian set events: %w", err)
+	}
+	defer tokensLockedSub.Unsubscribe()
 
 	errC := make(chan error)
 	logger := supervisor.Logger(ctx)
@@ -68,10 +85,13 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
 			select {
 			case <-ctx.Done():
 				return
-			case e := <-eventSubscription.Err():
-				errC <- e
+			case e := <-tokensLockedSub.Err():
+				errC <- fmt.Errorf("error while processing token lockup subscription: %w", e)
+				return
+			case e := <-guardianSetEvent.Err():
+				errC <- fmt.Errorf("error while processing guardian set subscription: %w", e)
 				return
-			case ev := <-sink:
+			case ev := <-tokensLockedC:
 				lock := &common.ChainLock{
 					TxHash:        ev.Raw.TxHash,
 					SourceAddress: ev.Sender,
@@ -91,6 +111,21 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
 					height: ev.Raw.BlockNumber,
 				}
 				e.pendingLocksGuard.Unlock()
+			case ev := <-guardianSetC:
+				logger.Info("guardian set has changed, fetching new value",
+					zap.Uint32("new_index", ev.NewGuardianIndex))
+
+				gs, err := caller.GetGuardianSet(&bind.CallOpts{Context: timeout}, ev.NewGuardianIndex)
+				if err != nil {
+					errC <- fmt.Errorf("error requesting new guardian set value: %w", err)
+					return
+				}
+
+				logger.Info("new guardian set fetched", zap.Any("value", gs), zap.Uint32("index", ev.NewGuardianIndex))
+				e.setChan <- &common.GuardianSet{
+					Keys: gs.Keys,
+					Index: ev.NewGuardianIndex,
+				}
 			}
 		}
 	}()
@@ -109,7 +144,7 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
 			case <-ctx.Done():
 				return
 			case e := <-headerSubscription.Err():
-				errC <- e
+				errC <- fmt.Errorf("error while processing header subscription: %w", e)
 				return
 			case ev := <-headSink:
 				start := time.Now()
@@ -132,7 +167,7 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
 						logger.Debug("lockup confirmed", zap.Stringer("tx", pLock.lock.TxHash),
 							zap.Stringer("number", ev.Number))
 						delete(e.pendingLocks, hash)
-						e.evChan <- pLock.lock
+						e.lockChan <- pLock.lock
 					}
 				}
 
@@ -145,6 +180,26 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
 
 	supervisor.Signal(ctx, supervisor.SignalHealthy)
 
+	// Fetch current guardian set
+	timeout, _ = context.WithTimeout(ctx, 15 * time.Second)
+	opts := &bind.CallOpts{Context: timeout}
+
+	currentIndex, err := caller.GuardianSetIndex(opts)
+	if err != nil {
+		return fmt.Errorf("error requesting current guardian set index: %w", err)
+	}
+
+	gs, err := caller.GetGuardianSet(opts, currentIndex)
+	if err != nil {
+		return fmt.Errorf("error requesting current guardian set value: %w", err)
+	}
+
+	logger.Info("current guardian set fetched", zap.Any("value", gs), zap.Uint32("index", currentIndex))
+	e.setChan <- &common.GuardianSet{
+		Keys: gs.Keys,
+		Index: currentIndex,
+	}
+
 	select {
 	case <-ctx.Done():
 		return ctx.Err()

+ 26 - 6
bridge/pkg/vaa/structs.go

@@ -2,14 +2,16 @@ package vaa
 
 import (
 	"bytes"
+	"crypto/ecdsa"
 	"encoding/binary"
 	"fmt"
-	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/crypto"
 	"io"
 	"math"
 	"math/big"
 	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/crypto"
 )
 
 type (
@@ -96,8 +98,8 @@ const (
 	supportedVAAVersion = 0x01
 )
 
-// ParseVAA deserializes the binary representation of a VAA
-func ParseVAA(data []byte) (*VAA, error) {
+// Unmarshal deserializes the binary representation of a VAA
+func Unmarshal(data []byte) (*VAA, error) {
 	if len(data) < minVAALength {
 		return nil, fmt.Errorf("VAA is too short")
 	}
@@ -215,8 +217,8 @@ func (v *VAA) VerifySignatures(addresses []common.Address) bool {
 	return true
 }
 
-// Serialize returns the binary representation of the VAA
-func (v *VAA) Serialize() ([]byte, error) {
+// Marshal returns the binary representation of the VAA
+func (v *VAA) Marshal() ([]byte, error) {
 	buf := new(bytes.Buffer)
 	MustWrite(buf, binary.BigEndian, v.Version)
 	MustWrite(buf, binary.BigEndian, v.GuardianSetIndex)
@@ -257,6 +259,24 @@ func (v *VAA) serializeBody() ([]byte, error) {
 	return buf.Bytes(), nil
 }
 
+func (v *VAA) AddSignature(key *ecdsa.PrivateKey, index uint8) {
+	data, err := v.SigningMsg()
+	if err != nil {
+		panic(err)
+	}
+	sig, err := crypto.Sign(data.Bytes(), key)
+	if err != nil {
+		panic(err)
+	}
+	sigData := [65]byte{}
+	copy(sigData[:], sig)
+
+	v.Signatures = append(v.Signatures, &Signature{
+		Index:     index,
+		Signature: sigData,
+	})
+}
+
 func parseBodyTransfer(r io.Reader) (*BodyTransfer, error) {
 	b := &BodyTransfer{}
 

+ 17 - 18
devnet/bridge.yaml

@@ -39,24 +39,23 @@ spec:
           image: guardiand-image
           command:
             - /guardiand
-            - -nodeKey
-            - /data/node.key
-            - -bootstrap
-            - /dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWQ1sV2kowPY1iJX1hJcVTysZjKv3sfULTGwhdpUGGZ1VF
             - -ethRPC
             - ws://eth-devnet:8545
-            - -ethContract
-            - 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
             - -ethConfirmations
-            - '2'
+            - '1'
             - -unsafeDevMode
+#            - -logLevel
+#            - debug
           ports:
             - containerPort: 8999
               name: p2p
               protocol: UDP
-          volumeMounts:
-            - name: guardian-data
-              mountPath: /data
+            - containerPort: 6060
+              name: pprof
+              protocol: TCP
+#          volumeMounts:
+#            - name: guardian-data
+#              mountPath: /data
 #        - name: agent
 #          image: solana-agent
 #          command:
@@ -70,11 +69,11 @@ spec:
 #            - containerPort: 9000
 #              name: grpc
 #              protocol: TCP
-  volumeClaimTemplates:
-    - metadata:
-        name: guardian-data
-      spec:
-        accessModes: [ "ReadWriteOnce" ]
-        resources:
-          requests:
-            storage: 1Gi
+#  volumeClaimTemplates:
+#    - metadata:
+#        name: guardian-data
+#      spec:
+#        accessModes: [ "ReadWriteOnce" ]
+#        resources:
+#          requests:
+#            storage: 1Gi

+ 1 - 0
ethereum/Dockerfile

@@ -11,6 +11,7 @@ RUN mkdir -p /home/node/.npm
 WORKDIR /home/node/app
 
 # Only invalidate the npm install step if package.json changed
+# TODO: build with "npm ci" for determinism
 ADD --chown=node:node package.json .
 
 # We want to cache node_modules *and* incorporate it into the final image.

+ 1 - 1
ethereum/src/send-lockups.js

@@ -36,7 +36,7 @@ module.exports = function(callback) {
             let block = await web3.eth.getBlock('latest');
             console.log("block", block.number, "with txs", block.transactions, "and time", block.timestamp);
             await advanceBlock();
-            await sleep(1000);
+            await sleep(5000);
         }
     }
 

+ 33 - 0
proto/gossip/v1/gossip.proto

@@ -4,7 +4,40 @@ package gossip.v1;
 
 option go_package = "proto/gossip/v1;gossipv1";
 
+message GossipMessage {
+  oneof message {
+    Heartbeat heartbeat = 1;
+    EthLockupObservation eth_lockup_observation = 2;
+  }
+}
+
+// P2P gossip heartbeats for network introspection purposes.
 message Heartbeat {
+  // The node's arbitrarily chosen, untrusted nodeName.
   string node_name = 1;
+  // A monotonic counter that resets to zero on startup.
   int64 counter = 2;
+  // UNIX wall time.
+  int64 timestamp = 3;
+
+  // TODO: include statement of gk public key?
+  // TODO: software version/release
+}
+
+// An EthLockupObservation is a signed statement by a given guardian node
+// that they observed a finalized lockup on Ethereum.
+//
+// The lockup is uniquely identified by its hashed (tx_hash, nonce, values...) tuple.
+//
+// Other nodes will verify the signature. Once any node has observed a quorum of
+// guardians submitting valid signatures for a given hash, they can be assembled into a VAA.
+//
+// Messages without valid signature are dropped unceremoniously.
+message EthLockupObservation {
+  // Guardian pubkey as truncated eth address.
+  bytes addr = 1;
+  // The lockup's deterministic, unique hash. See pkg/common/chainlock.go.
+  bytes hash = 2;
+  // ECSDA signature of the hash using the node's guardian key.
+  bytes signature = 3;
 }