-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a skeleton eth server client. The client connects over ipc and relays new blocks to the server.
- Loading branch information
1 parent
6642f7b
commit 5344f20
Showing
10 changed files
with
1,102 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 := ðBlock{ | ||
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] = ð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{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// 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 ethFetcher interface. | ||
var _ ethFetcher = (*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() { | ||
if c.ec != nil { | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
ethClient.Shutdown() | ||
}() | ||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.