From 0c433e8ba7027ceb446e505076d4cc275351f148 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Mon, 15 Feb 2021 13:30:35 +0900 Subject: [PATCH] multi: Add Ethereum server asset. --- dex/testing/dcrdex/harness.sh | 21 +- dex/testing/eth/create-node.sh | 2 +- dex/testing/eth/harness.sh | 5 + go.mod | 1 + go.sum | 172 +++++++++++- server/asset/eth/cache.go | 133 +++++++++ server/asset/eth/client.go | 70 +++++ server/asset/eth/client_harness_test.go | 61 +++++ server/asset/eth/config.go | 47 ++++ server/asset/eth/eth.go | 350 ++++++++++++++++++++++++ server/asset/eth/eth_test.go | 227 +++++++++++++++ server/cmd/dcrdex/main.go | 1 + 12 files changed, 1086 insertions(+), 4 deletions(-) create mode 100644 server/asset/eth/cache.go create mode 100644 server/asset/eth/client.go create mode 100644 server/asset/eth/client_harness_test.go create mode 100644 server/asset/eth/config.go create mode 100644 server/asset/eth/eth.go create mode 100644 server/asset/eth/eth_test.go diff --git a/dex/testing/dcrdex/harness.sh b/dex/testing/dcrdex/harness.sh index 060ffd6b5a..ee01c9bda9 100755 --- a/dex/testing/dcrdex/harness.sh +++ b/dex/testing/dcrdex/harness.sh @@ -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 @@ -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. @@ -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" } } @@ -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 diff --git a/dex/testing/eth/create-node.sh b/dex/testing/eth/create-node.sh index b6048630aa..86076bd015 100755 --- a/dex/testing/eth/create-node.sh +++ b/dex/testing/eth/create-node.sh @@ -37,7 +37,7 @@ cat > "${NODES_ROOT}/harness-ctl/mine-${NAME}" < 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] = ðBlock{ + 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{} +} diff --git a/server/asset/eth/client.go b/server/asset/eth/client.go new file mode 100644 index 0000000000..554d41c4b8 --- /dev/null +++ b/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 +} diff --git a/server/asset/eth/client_harness_test.go b/server/asset/eth/client_harness_test.go new file mode 100644 index 0000000000..71af114814 --- /dev/null +++ b/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) + } +} diff --git a/server/asset/eth/config.go b/server/asset/eth/config.go new file mode 100644 index 0000000000..1f921820c9 --- /dev/null +++ b/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 +} diff --git a/server/asset/eth/eth.go b/server/asset/eth/eth.go new file mode 100644 index 0000000000..94103ec648 --- /dev/null +++ b/server/asset/eth/eth.go @@ -0,0 +1,350 @@ +// 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" + "sync" + "time" + + "decred.org/dcrdex/dex" + "decred.org/dcrdex/server/asset" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func init() { + asset.Register(assetName, &Driver{}) +} + +const ( + assetName = "eth" + coinIDSize = 52 + // The blockPollInterval is the delay between calls to BestBlockHash to + // check for new blocks. + blockPollInterval = time.Second +) + +var zeroHash = common.Hash{} + +// Driver implements asset.Driver. +type Driver struct{} + +// Setup creates the ETH backend. Start the backend with its Run method. +func (d *Driver) Setup(configPath string, logger dex.Logger, network dex.Network) (asset.Backend, error) { + return NewBackend(configPath, logger, network) +} + +// DecodeCoinID creates a human-readable representation of a coin ID for Ethereum. +func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { + return coinIDToString(coinID) +} + +// ethNode represents a blockchain information fetcher. In practice, it is +// satisfied by client. For testing, it can be satisfied by a stub. +type ethNode interface { + Shutdown() + Connect(ctx context.Context, IPC string) error + BestBlockHash(ctx context.Context) (common.Hash, error) + Block(ctx context.Context, hash common.Hash) (*types.Block, error) +} + +// Backend is an asset backend for Ethereum. It has methods for fetching output +// information and subscribing to block updates. It maintains a cache of block +// data for quick lookups. Backend implements asset.Backend, so provides +// exported methods for DEX-related blockchain info. +type Backend struct { + cfg *config + nodeInitted bool + node ethNode + // The backend provides block notification channels through the BlockChannel + // method. signalMtx locks the blockChans array. + signalMtx sync.RWMutex + blockChans map[chan *asset.BlockUpdate]struct{} + // The block cache stores just enough info about the blocks to prevent future + // calls to Block. + blockCache *blockCache + // A logger will be provided by the DEX. All logging should use the provided + // logger. + log dex.Logger +} + +// Check that Backend satisfies the Backend interface. +var _ asset.Backend = (*Backend)(nil) + +// unconnectedETH returns a Backend without a node. The node should be set +// before use. +func unconnectedETH(logger dex.Logger, cfg *config) *Backend { + return &Backend{ + cfg: cfg, + blockCache: newBlockCache(logger), + log: logger, + blockChans: make(map[chan *asset.BlockUpdate]struct{}), + } +} + +// NewBackend is the exported constructor by which the DEX will import the +// Backend. +func NewBackend(ipc string, logger dex.Logger, network dex.Network) (*Backend, error) { + cfg, err := load(ipc, network) + if err != nil { + return nil, err + } + return unconnectedETH(logger, cfg), nil +} + +func (eth *Backend) shutdown() { + if eth.nodeInitted { + eth.node.Shutdown() + } +} + +// Connect connects to the node RPC server. A dex.Connector. +func (eth *Backend) Connect(ctx context.Context) (*sync.WaitGroup, error) { + c := client{} + if err := c.Connect(ctx, eth.cfg.IPC); err != nil { + return nil, err + } + eth.node = &c + eth.nodeInitted = true + var wg sync.WaitGroup + wg.Add(1) + go func() { + eth.run(ctx) + wg.Done() + }() + return &wg, nil +} + +// InitTxSize is an asset.Backend method that must produce the max size of a +// standardized atomic swap initialization transaction. +func (eth *Backend) InitTxSize() uint32 { + return 0 +} + +// InitTxSizeBase is InitTxSize not including an input. +func (eth *Backend) InitTxSizeBase() uint32 { + return 0 +} + +// FeeRate returns the current optimal fee rate in atoms / byte. +func (eth *Backend) FeeRate() (uint64, error) { + return 0, nil +} + +// BlockChannel creates and returns a new channel on which to receive block +// updates. If the returned channel is ever blocking, there will be no error +// logged from the eth package. Part of the asset.Backend interface. +func (eth *Backend) BlockChannel(size int) <-chan *asset.BlockUpdate { + c := make(chan *asset.BlockUpdate, size) + eth.signalMtx.Lock() + defer eth.signalMtx.Unlock() + eth.blockChans[c] = struct{}{} + return c +} + +// Contract is part of the asset.Backend interface. +func (eth *Backend) Contract(coinID []byte, redeemScript []byte) (*asset.Contract, error) { + return nil, nil +} + +// ValidateSecret checks that the secret satisfies the contract. +func (eth *Backend) ValidateSecret(secret, contract []byte) bool { + return false +} + +// Synced is true if the blockchain is ready for action. +func (eth *Backend) Synced() (bool, error) { + return false, nil +} + +// Redemption is an input that redeems a swap contract. +func (eth *Backend) Redemption(redemptionID, contractID []byte) (asset.Coin, error) { + return nil, nil +} + +// FundingCoin is an unspent output. +func (eth *Backend) FundingCoin(ctx context.Context, coinID []byte, redeemScript []byte) (asset.FundingCoin, error) { + return nil, nil +} + +// ValidateCoinID attempts to decode the coinID. +func (eth *Backend) ValidateCoinID(coinID []byte) (string, error) { + return coinIDToString(coinID) +} + +// ValidateContract ensures that the swap contract is constructed properly, and +// contains valid sender and receiver addresses. +func (eth *Backend) ValidateContract(contract []byte) error { + return nil +} + +// CheckAddress checks that the given address is parseable. +func (eth *Backend) CheckAddress(addr string) bool { + return common.IsHexAddress(addr) +} + +// VerifyUnspentCoin attempts to verify a coin ID is unspent. +func (eth *Backend) VerifyUnspentCoin(ctx context.Context, coinID []byte) error { + return nil +} + +// run processes the queue and monitors the application context. +func (eth *Backend) run(ctx context.Context) { + var wg sync.WaitGroup + wg.Add(1) + // Shut down the RPC client on ctx.Done(). + go func() { + <-ctx.Done() + eth.shutdown() + wg.Done() + }() + + blockPoll := time.NewTicker(blockPollInterval) + defer blockPoll.Stop() + addBlock := func(block *types.Block, reorg bool) { + _, err := eth.blockCache.add(block) + if err != nil { + eth.log.Errorf("error adding new best block to cache: %v", err) + } + eth.signalMtx.Lock() + eth.log.Tracef("Notifying %d eth asset consumers of new block at height %d", + len(eth.blockChans), block.NumberU64()) + for c := range eth.blockChans { + select { + case c <- &asset.BlockUpdate{ + Err: nil, + Reorg: reorg, + }: + default: + // Commented to try sends on future blocks. + // close(c) + // delete(eth.blockChans, c) + // + // TODO: Allow the receiver (e.g. Swapper.Run) to inform done + // status so the channels can be retired cleanly rather than + // trying them forever. + } + } + eth.signalMtx.Unlock() + } + + sendErr := func(err error) { + eth.log.Error(err) + eth.signalMtx.Lock() + for c := range eth.blockChans { + select { + case c <- &asset.BlockUpdate{ + Err: err, + }: + default: + eth.log.Errorf("failed to send sending block update on blocking channel") + // close(c) + // delete(eth.blockChans, c) + } + } + eth.signalMtx.Unlock() + } + + sendErrFmt := func(s string, a ...interface{}) { + sendErr(fmt.Errorf(s, a...)) + } + +out: + for { + select { + case <-blockPoll.C: + tip := eth.blockCache.tip() + bestHash, err := eth.node.BestBlockHash(ctx) + if err != nil { + sendErr(asset.NewConnectionError("error retrieving best block: %w", err)) + continue + } + if bestHash == tip.hash { + continue + } + + block, err := eth.node.Block(ctx, bestHash) + if err != nil { + sendErrFmt("error retrieving block %x: %w", bestHash, err) + continue + } + // If this doesn't build on the best known block, look for a reorg. + prevHash := block.ParentHash() + // If it builds on the best block or the cache is empty, it's good to add. + if prevHash == tip.hash || tip.height == 0 { + eth.log.Debugf("New block %x (%d)", bestHash, block.NumberU64()) + addBlock(block, false) + continue + } + + // It is either a reorg, or the previous block is not the cached + // best block. Crawl blocks backwards until finding a mainchain + // block, flagging blocks from the cache as orphans along the way. + // + // TODO: Fix this. The exact ethereum behavior here is yet unknown. + iHash := tip.hash + reorgHeight := uint64(0) + for { + if iHash == zeroHash { + break + } + iBlock, err := eth.node.Block(ctx, iHash) + if err != nil { + sendErrFmt("error retrieving block %s: %w", iHash, err) + break + } + if iBlock.NumberU64() == 0 { + break + } + reorgHeight = iBlock.NumberU64() + iHash = iBlock.ParentHash() + } + + var reorg bool + if reorgHeight > 0 { + reorg = true + eth.log.Infof("Tip change from %s (%d) to %s (%d) detected (reorg or just fast blocks).", + tip.hash, tip.height, bestHash, block.NumberU64()) + eth.blockCache.reorg(reorgHeight) + } + + // Now add the new block. + addBlock(block, reorg) + + case <-ctx.Done(): + break out + } + } + // Wait for the RPC client to shut down. + wg.Wait() +} + +// decodeCoinID decodes the coin ID into a contract address and secret hash. +func decodeCoinID(coinID []byte) (common.Address, []byte, error) { + if len(coinID) != coinIDSize { + return common.Address{}, nil, fmt.Errorf("coin ID wrong length. expected %d, got %d", + coinIDSize, len(coinID)) + } + secretHash := make([]byte, 32) + copy(secretHash, coinID[20:]) + return common.BytesToAddress(coinID[:20]), secretHash, nil +} + +func coinIDToString(coinID []byte) (string, error) { + addr, secretHash, err := decodeCoinID(coinID) + if err != nil { + return "", err + } + return fmt.Sprintf("%x:%x", addr, secretHash), err +} + +// toCoinID converts the address and secret hash to a coin ID. +func toCoinID(addr *common.Address, secretHash []byte) []byte { + b := make([]byte, coinIDSize) + copy(b[:], addr[:]) + copy(b[20:], secretHash[:]) + return b +} diff --git a/server/asset/eth/eth_test.go b/server/asset/eth/eth_test.go new file mode 100644 index 0000000000..919d4c2c1b --- /dev/null +++ b/server/asset/eth/eth_test.go @@ -0,0 +1,227 @@ +// +build !dcrlive +// +// These tests will not be run if the dcrlive build tag is set. + +package eth + +import ( + "context" + "math/big" + "reflect" + "testing" + + "decred.org/dcrdex/dex" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +var ( + _ ethNode = (*testNode)(nil) + tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) +) + +type testNode struct { + connectErr error + bestBlockHash common.Hash + bestBlockHashErr error + block *types.Block + blockErr error +} + +func (n *testNode) Connect(ctx context.Context, IPC string) error { + return n.connectErr +} + +func (n *testNode) Shutdown() {} + +func (n *testNode) BestBlockHash(ctx context.Context) (common.Hash, error) { + return n.bestBlockHash, n.bestBlockHashErr +} + +func (n *testNode) Block(ctx context.Context, hash common.Hash) (*types.Block, error) { + return n.block, n.blockErr +} + +func TestLoad(t *testing.T) { + tests := []struct { + name, IPC, wantIPC string + network dex.Network + wantErr bool + }{{ + name: "ok ipc supplied", + IPC: "/home/john/bleh.ipc", + wantIPC: "/home/john/bleh.ipc", + network: dex.Simnet, + }, { + name: "ok ipc supplied", + IPC: "", + wantIPC: defaultIPC, + network: dex.Simnet, + }, { + name: "mainnet not allowed", + IPC: "", + wantIPC: defaultIPC, + network: dex.Mainnet, + wantErr: true, + }} + + for _, test := range tests { + cfg, err := load(test.IPC, test.network) + if test.wantErr { + if err == nil { + t.Fatalf("expected error for test %v", test.name) + } + continue + } + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + if cfg.IPC != test.wantIPC { + t.Fatalf("want ipc value of %v but got %v for test %v", test.wantIPC, cfg.IPC, test.name) + } + } +} + +func TestDecodeCoinID(t *testing.T) { + tests := []struct { + name string + wantAddr common.Address + coinID, wantSecretHash []byte + wantErr bool + }{{ + name: "ok", + coinID: []byte{ + 0x18, 0xd6, 0x5f, 0xb8, 0xd6, 0x0c, 0x11, 0x99, 0xbb, + 0x1a, 0xd3, 0x81, 0xbe, 0x47, 0xaa, 0x69, 0x2b, 0x48, + 0x26, 0x05, // 20 byte addr + 0x71, 0xd8, 0x10, 0xd3, 0x93, 0x33, 0x29, 0x6b, 0x51, + 0x8c, 0x84, 0x6a, 0x3e, 0x49, 0xec, 0xa5, 0x5f, 0x99, + 0x8f, 0xd7, 0x99, 0x49, 0x98, 0xbb, 0x3e, 0x50, 0x48, + 0x56, 0x7f, 0x2f, 0x07, 0x3c, // 32 byte secret hash + }, + wantAddr: common.Address{ + 0x18, 0xd6, 0x5f, 0xb8, 0xd6, 0x0c, 0x11, 0x99, 0xbb, + 0x1a, 0xd3, 0x81, 0xbe, 0x47, 0xaa, 0x69, 0x2b, 0x48, + 0x26, 0x05, + }, + wantSecretHash: []byte{ + 0x71, 0xd8, 0x10, 0xd3, 0x93, 0x33, 0x29, 0x6b, 0x51, + 0x8c, 0x84, 0x6a, 0x3e, 0x49, 0xec, 0xa5, 0x5f, 0x99, + 0x8f, 0xd7, 0x99, 0x49, 0x98, 0xbb, 0x3e, 0x50, 0x48, + 0x56, 0x7f, 0x2f, 0x07, 0x3c, // 32 byte secret hash + }, + }, { + name: "wrong length", + coinID: []byte{ + 0x18, 0xd6, 0x5f, 0xb8, 0xd6, 0x0c, 0x11, 0x99, 0xbb, + 0x1a, 0xd3, 0x81, 0xbe, 0x47, 0xaa, 0x69, 0x2b, 0x48, + 0x26, 0x05, // 20 byte addr + 0x71, 0xd8, 0x10, 0xd3, 0x93, 0x33, 0x29, 0x6b, 0x51, + 0x8c, 0x84, 0x6a, 0x3e, 0x49, 0xec, 0xa5, 0x5f, 0x99, + 0x8f, 0xd7, 0x99, 0x49, 0x98, 0xbb, 0x3e, 0x50, 0x48, + 0x56, 0x7f, 0x2f, 0x07, // 31 bytes + }, + wantErr: true, + }} + + for _, test := range tests { + addr, secretHash, err := decodeCoinID(test.coinID) + if test.wantErr { + if err == nil { + t.Fatalf("expected error for test %v", test.name) + } + continue + } + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + if addr != test.wantAddr { + t.Fatalf("want addr value of %v but got %v for test %v", + test.wantAddr, addr, test.name) + } + if !reflect.DeepEqual(secretHash, test.wantSecretHash) { + t.Fatalf("want secret hash value of %v but got %v for test %v", + test.wantSecretHash, secretHash, test.name) + } + } +} + +func TestCoinIDToString(t *testing.T) { + addr := "18d65fb8d60c1199bb1ad381be47aa692b482605" + secretHash := "71d810d39333296b518c846a3e49eca55f998fd7994998bb3e5048567f2f073c" + tests := []struct { + name, wantCoinID string + coinID []byte + wantErr bool + }{{ + name: "ok", + coinID: []byte{ + 0x18, 0xd6, 0x5f, 0xb8, 0xd6, 0x0c, 0x11, 0x99, 0xbb, + 0x1a, 0xd3, 0x81, 0xbe, 0x47, 0xaa, 0x69, 0x2b, 0x48, + 0x26, 0x05, // 20 byte addr + 0x71, 0xd8, 0x10, 0xd3, 0x93, 0x33, 0x29, 0x6b, 0x51, + 0x8c, 0x84, 0x6a, 0x3e, 0x49, 0xec, 0xa5, 0x5f, 0x99, + 0x8f, 0xd7, 0x99, 0x49, 0x98, 0xbb, 0x3e, 0x50, 0x48, + 0x56, 0x7f, 0x2f, 0x07, 0x3c, // 32 byte secret hash + }, + wantCoinID: addr + ":" + secretHash, + }, { + name: "wrong length", + coinID: []byte{ + 0x18, 0xd6, 0x5f, 0xb8, 0xd6, 0x0c, 0x11, 0x99, 0xbb, + 0x1a, 0xd3, 0x81, 0xbe, 0x47, 0xaa, 0x69, 0x2b, 0x48, + 0x26, 0x05, // 20 byte addr + 0x71, 0xd8, 0x10, 0xd3, 0x93, 0x33, 0x29, 0x6b, 0x51, + 0x8c, 0x84, 0x6a, 0x3e, 0x49, 0xec, 0xa5, 0x5f, 0x99, + 0x8f, 0xd7, 0x99, 0x49, 0x98, 0xbb, 0x3e, 0x50, 0x48, + 0x56, 0x7f, 0x2f, 0x07, // 31 bytes + }, + wantErr: true, + }} + + for _, test := range tests { + coinID, err := coinIDToString(test.coinID) + if test.wantErr { + if err == nil { + t.Fatalf("expected error for test %v", test.name) + } + continue + } + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + if coinID != test.wantCoinID { + t.Fatalf("want coinID value of %v but got %v for test %v", + test.wantCoinID, coinID, test.name) + } + } +} + +func TestRun(t *testing.T) { + // TODO: Test all paths. + ctx, cancel := context.WithCancel(context.Background()) + header1 := &types.Header{Number: big.NewInt(1)} + block1 := types.NewBlockWithHeader(header1) + blockHash1 := block1.Hash() + node := &testNode{} + node.bestBlockHash = blockHash1 + node.block = block1 + backend := unconnectedETH(tLogger, nil) + ch := backend.BlockChannel(1) + blocker := make(chan struct{}) + backend.node = node + go func() { + <-ch + cancel() + close(blocker) + }() + backend.run(ctx) + <-blocker + backend.blockCache.mtx.Lock() + best := backend.blockCache.best + backend.blockCache.mtx.Unlock() + if best.hash != blockHash1 { + t.Fatalf("want header hash %x but got %x", blockHash1, best.hash) + } + cancel() +} diff --git a/server/cmd/dcrdex/main.go b/server/cmd/dcrdex/main.go index 0f1c77f6bb..ac82077c27 100644 --- a/server/cmd/dcrdex/main.go +++ b/server/cmd/dcrdex/main.go @@ -21,6 +21,7 @@ import ( _ "decred.org/dcrdex/server/asset/bch" _ "decred.org/dcrdex/server/asset/btc" // register btc asset _ "decred.org/dcrdex/server/asset/dcr" // register dcr asset + _ "decred.org/dcrdex/server/asset/eth" // register eth asset _ "decred.org/dcrdex/server/asset/ltc" // register ltc asset dexsrv "decred.org/dcrdex/server/dex" "github.com/decred/dcrd/dcrec/secp256k1/v3"