Skip to content

Commit

Permalink
multi: Add Ethereum server asset.
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Mar 2, 2021
1 parent c9c30c1 commit 0c433e8
Show file tree
Hide file tree
Showing 12 changed files with 1,086 additions and 4 deletions.
21 changes: 19 additions & 2 deletions dex/testing/dcrdex/harness.sh
Expand Up @@ -21,7 +21,7 @@ TEST_DB=dcrdex_simnet_test
sudo -u postgres -H psql -c "DROP DATABASE IF EXISTS ${TEST_DB}" \
-c "CREATE DATABASE ${TEST_DB} OWNER dcrdex"

EPOCH_DURATION=${EPOCH:-15000}
EPOCH_DURATION=${EPOCH:-15000}
if [ "${EPOCH_DURATION}" -lt 1000 ]; then
echo "epoch duration cannot be < 1000 ms"
exit 1
Expand All @@ -37,6 +37,9 @@ BCH_ON=$?
~/dextest/ltc/harness-ctl/alpha getblockchaininfo > /dev/null
LTC_ON=$?

~/dextest/eth/harness-ctl/alpha attach --exec 'eth.blockNumber' > /dev/null
ETH_ON=$?

set -e

# Write markets.json.
Expand Down Expand Up @@ -125,6 +128,20 @@ if [ $BCH_ON -eq 0 ]; then
EOF
fi

if [ $ETH_ON -eq 0 ]; then
cat << EOF >> "./markets.json"
},
"ETH_simnet": {
"bip44symbol": "eth",
"network": "simnet",
"lotSize": 1000000,
"rateStep": 1000000,
"maxFeeRate": 20,
"swapConf": 2,
"configPath": "${TEST_ROOT}/eth/alpha/node/geth.ipc"
EOF
fi

cat << EOF >> "./markets.json"
}
}
Expand All @@ -147,7 +164,7 @@ adminsrvaddr=127.0.0.1:16542
bcasttimeout=1m
EOF

# Set the postgres user pass if provided.
# Set the postgres user pass if provided.
if [ -n "${PG_PASS}" ]; then
echo pgpass="${PG_PASS}" >> ./dcrdex.conf
fi
Expand Down
2 changes: 1 addition & 1 deletion dex/testing/eth/create-node.sh
Expand Up @@ -37,7 +37,7 @@ cat > "${NODES_ROOT}/harness-ctl/mine-${NAME}" <<EOF
*) NUM=\$1 ;;
esac
for i in \$(seq \$NUM) ; do
./${NAME} attach --preload "${MINE_JS}" --exec 'mine()'
"${NODES_ROOT}/harness-ctl/${NAME}" attach --preload "${MINE_JS}" --exec 'mine()'
done
EOF
chmod +x "${NODES_ROOT}/harness-ctl/mine-${NAME}"
Expand Down
5 changes: 5 additions & 0 deletions dex/testing/eth/harness.sh
Expand Up @@ -121,6 +121,11 @@ echo "Starting simnet beta node"
echo "Connecting nodes"
"${NODES_ROOT}/harness-ctl/alpha" "attach --exec admin.addPeer('enode://${BETA_ENODE}@127.0.0.1:$BETA_NODE_PORT')"

sleep 1

echo "Mining a block"
"${NODES_ROOT}/harness-ctl/mine-alpha" "1"

# Reenable history and attach to the control session.
tmux select-window -t $SESSION:0
tmux send-keys -t $SESSION:0 "set -o history" C-m
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -24,6 +24,7 @@ require (
github.com/decred/dcrd/wire v1.4.0
github.com/decred/go-socks v1.1.0
github.com/decred/slog v1.1.0
github.com/ethereum/go-ethereum v1.9.25
github.com/gcash/bchd v0.17.2-0.20201218180520-5708823e0e99
github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000
github.com/go-chi/chi v1.5.1
Expand Down
172 changes: 171 additions & 1 deletion go.sum

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions server/asset/eth/cache.go
@@ -0,0 +1,133 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package eth

import (
"sync"

"decred.org/dcrdex/dex"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

// The ethBlock structure should hold a minimal amount of information about a
// block.
type ethBlock struct {
hash common.Hash
height uint64
orphaned bool
}

// The blockCache caches block information to prevent repeated calls to
// rpcclient.GetblockVerbose.
type blockCache struct {
mtx sync.RWMutex
blocks map[common.Hash]*ethBlock
mainchain map[uint64]*ethBlock
best ethBlock
log dex.Logger
}

// Constructor for a blockCache.
func newBlockCache(logger dex.Logger) *blockCache {
return &blockCache{
blocks: make(map[common.Hash]*ethBlock),
mainchain: make(map[uint64]*ethBlock),
log: logger,
}
}

// Getter for a block by it's hash.
func (cache *blockCache) block(h common.Hash) (*ethBlock, bool) {
cache.mtx.RLock()
defer cache.mtx.RUnlock()
blk, found := cache.blocks[h]
return blk, found
}

// Getter for a mainchain block by its height. This method does not attempt
// to load the block from the blockchain if it is not found.
func (cache *blockCache) atHeight(height uint64) (*ethBlock, bool) {
cache.mtx.RLock()
defer cache.mtx.RUnlock()
blk, found := cache.mainchain[height]
return blk, found
}

// Add a block to the blockCache. This method will translate the RPC result
// to a ethBlock, returning the ethBlock. If the block is not orphaned, it will
// be added to the mainchain.
func (cache *blockCache) add(block *types.Block) (*ethBlock, error) {
cache.mtx.Lock()
defer cache.mtx.Unlock()
// TODO: Fix this.
orphaned := false
height, hash := block.NumberU64(), block.Hash()
blk := &ethBlock{
hash: hash,
height: height,
orphaned: orphaned,
}
cache.blocks[hash] = blk

if !orphaned {
cache.mainchain[height] = blk
if height > cache.best.height {
cache.best.height = height
cache.best.hash = hash
}
}
return blk, nil
}

// Get the best known block height for the blockCache.
func (cache *blockCache) tipHeight() uint64 {
cache.mtx.Lock()
defer cache.mtx.Unlock()
return cache.best.height
}

// Get the best known block hash in the blockCache.
func (cache *blockCache) tipHash() common.Hash {
cache.mtx.RLock()
defer cache.mtx.RUnlock()
return cache.best.hash
}

// Get the best known block height in the blockCache.
func (cache *blockCache) tip() ethBlock {
cache.mtx.RLock()
defer cache.mtx.RUnlock()
return cache.best
}

// Trigger a reorg, setting any blocks at or above the provided height as
// orphaned and removing them from mainchain, but not the blocks map. reorg
// clears the best block, so should always be followed with the addition of a
// new mainchain block.
func (cache *blockCache) reorg(from uint64) {
if from < 0 {
return
}
cache.mtx.Lock()
defer cache.mtx.Unlock()
for height := from; height <= cache.best.height; height++ {
block, found := cache.mainchain[height]
if !found {
cache.log.Errorf("reorg block not found on mainchain at height %d for a reorg from %d to %d", height, from, cache.best.height)
continue
}
// Delete the block from mainchain.
delete(cache.mainchain, block.height)
// Store an orphaned block in the blocks cache.
cache.blocks[block.hash] = &ethBlock{
hash: block.hash,
height: block.height,
orphaned: true,
}
}
// Set this to a zero block so that the new block will replace it even if
// it is of the same height as the previous best block.
cache.best = ethBlock{}
}
70 changes: 70 additions & 0 deletions server/asset/eth/client.go
@@ -0,0 +1,70 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package eth

import (
"context"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
)

// Check that client satisfies the ethNode interface.
var _ ethNode = (*client)(nil)

type client struct {
ec *ethclient.Client
}

// Connect connects to an ipc socket. It then wraps ethclient's client and
// bundles commands in a form we can easil use.
func (c *client) Connect(ctx context.Context, IPC string) error {
client, err := rpc.DialIPC(ctx, IPC)
if err != nil {
return fmt.Errorf("unable to dial rpc: %v", err)
}
ec := ethclient.NewClient(client)
c.ec = ec
return nil
}

// Shutdown shuts down the client.
func (c *client) Shutdown() {
c.ec.Close()
}

// BestBlockHash gets the best blocks hash at the time of calling. Due to the
// speed of Ethereum blocks, this changes often.
func (c *client) BestBlockHash(ctx context.Context) (common.Hash, error) {
header, err := c.bestHeader(ctx)
if err != nil {
return common.Hash{}, err
}
return header.Hash(), nil
}

func (c *client) bestHeader(ctx context.Context) (*types.Header, error) {
bn, err := c.ec.BlockNumber(ctx)
if err != nil {
return nil, err
}
header, err := c.ec.HeaderByNumber(ctx, big.NewInt(int64(bn)))
if err != nil {
return nil, err
}
return header, nil
}

// Block gets the block identified by hash.
func (c *client) Block(ctx context.Context, hash common.Hash) (*types.Block, error) {
block, err := c.ec.BlockByHash(ctx, hash)
if err != nil {
return nil, err
}
return block, nil
}
61 changes: 61 additions & 0 deletions server/asset/eth/client_harness_test.go
@@ -0,0 +1,61 @@
// +build harness
//
// This test requires that the testnet harness be running and the unix socket
// be located at $HOME/dextest/eth/alpha/node/geth.ipc

package eth

import (
"fmt"
"os"
"path/filepath"

"context"
"testing"
)

var (
homeDir = os.Getenv("HOME")
ipc = filepath.Join(homeDir, "dextest/eth/alpha/node/geth.ipc")
ethClient = new(client)
ctx context.Context
)

func TestMain(m *testing.M) {
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(context.Background())
defer func() {
cancel()
}()

if err := ethClient.Connect(ctx, ipc); err != nil {
fmt.Printf("Connect error: %v\n", err)
os.Exit(1)
}
os.Exit(m.Run())
}

func TestBestBlockHash(t *testing.T) {
_, err := ethClient.BestBlockHash(ctx)
if err != nil {
t.Fatal(err)
}
}

func TestBestHeader(t *testing.T) {
_, err := ethClient.bestHeader(ctx)
if err != nil {
t.Fatal(err)
}
}

func TestBlock(t *testing.T) {
h, err := ethClient.BestBlockHash(ctx)
if err != nil {
t.Fatal(err)
}
_, err = ethClient.Block(ctx, h)
if err != nil {
t.Fatal(err)
}
}
47 changes: 47 additions & 0 deletions server/asset/eth/config.go
@@ -0,0 +1,47 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package eth

import (
"fmt"
"path/filepath"

"decred.org/dcrdex/dex"
"github.com/decred/dcrd/dcrutil/v3"
)

var (
ethHomeDir = dcrutil.AppDataDir("ethereum", false)
defaultIPC = filepath.Join(ethHomeDir, "geth/geth.ipc")
)

type config struct {
// IPC is the location of the inner process communication socket.
IPC string `long:"ipc" description:"Location of the geth ipc socket."`
}

// load checks the network and sets the ipc location if not supplied.
//
// TODO: Test this with windows.
func load(IPC string, network dex.Network) (*config, error) {
switch network {
case dex.Simnet:
case dex.Testnet:
case dex.Mainnet:
// TODO: Allow.
return nil, fmt.Errorf("eth cannot be used on mainnet")
default:
return nil, fmt.Errorf("unknown network ID: %d", uint8(network))
}

cfg := &config{
IPC: IPC,
}

if cfg.IPC == "" {
cfg.IPC = defaultIPC
}

return cfg, nil
}

0 comments on commit 0c433e8

Please sign in to comment.