Skip to content

Commit

Permalink
Merge pull request ethereum#26 from prestonvanloon/collator-tests
Browse files Browse the repository at this point in the history
Add basic collator tests with interface refactoring
  • Loading branch information
rauljordan authored Feb 10, 2018
2 parents 7afef58 + d889fe7 commit 2ecf016
Show file tree
Hide file tree
Showing 6 changed files with 718 additions and 42 deletions.
8 changes: 8 additions & 0 deletions sharding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ The project consists of the following parts, with each of them requiring compreh

The VMC is built in Solidity and deployed to the geth node upon launch of the client if it does not exist in the network at a specified address. If the contract already exists, the client simply sets up an interface to programmatically call the internal contract functions and listens to transactions broadcasted to the geth node to begin the sharding system.

#### Generating Contract Bindings

To generate the go bindings run the following command from this package:

```bash
go generate
```

### VMC Wrapper & Sharding Client

As we will be interacting with a geth node, we will create a Golang interface that wraps over the VMC and a client that connects to the local geth node upon launch via JSON-RPC.
Expand Down
33 changes: 33 additions & 0 deletions sharding/client.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package sharding

//go:generate abigen --sol contracts/validator_manager.sol --pkg contracts --out contracts/validator_manager.go

import (
"context"
"fmt"
"io/ioutil"
"math/big"
"strings"

"github.com/ethereum/go-ethereum"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
Expand Down Expand Up @@ -164,6 +168,35 @@ func (c *Client) createTXOps(value *big.Int) (bind.TransactOpts, error) {

}

// Account to use for sharding transactions.
func (c *Client) Account() (*accounts.Account, error) {
accounts := c.keystore.Accounts()
if len(accounts) == 0 {
return nil, fmt.Errorf("no accounts found")
}

if err := c.unlockAccount(accounts[0]); err != nil {
return nil, fmt.Errorf("cannot unlock account. %v", err)
}

return &accounts[0], nil
}

// ChainReader for interacting with the chain.
func (c *Client) ChainReader() ethereum.ChainReader {
return ethereum.ChainReader(c.client)
}

// Client to interact with ethereum node.
func (c *Client) Client() *ethclient.Client {
return c.client
}

// VMCCaller to interact with the validator management contract.
func (c *Client) VMCCaller() *contracts.VMCCaller {
return &c.vmc.VMCCaller
}

// dialRPC endpoint to node.
func dialRPC(endpoint string) (*rpc.Client, error) {
if endpoint == "" {
Expand Down
36 changes: 22 additions & 14 deletions sharding/collator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,31 @@ package sharding
import (
"context"
"fmt"
"math/big"

ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"math/big"
"github.com/ethereum/go-ethereum/sharding/contracts"
)

type collatorClient interface {
Account() (*accounts.Account, error)
ChainReader() ethereum.ChainReader
VMCCaller() *contracts.VMCCaller
}

// SubscribeBlockHeaders checks incoming block headers and determines if
// we are an eligible proposer for collations. Then, it finds the pending tx's
// from the running geth node and sorts them by descending order of gas price,
// eliminates those that ask for too much gas, and routes them over
// to the VMC to create a collation
func subscribeBlockHeaders(c *Client) error {
func subscribeBlockHeaders(c collatorClient) error {
headerChan := make(chan *types.Header, 16)

_, err := c.client.SubscribeNewHead(context.Background(), headerChan)
_, err := c.ChainReader().SubscribeNewHead(context.Background(), headerChan)
if err != nil {
return fmt.Errorf("unable to subscribe to incoming headers. %v", err)
}
Expand All @@ -42,27 +52,25 @@ func subscribeBlockHeaders(c *Client) error {
// collation for the available shards in the VMC. The function calls
// getEligibleProposer from the VMC and proposes a collation if
// conditions are met
func checkShardsForProposal(c *Client, head *types.Header) error {

accounts := c.keystore.Accounts()
if len(accounts) == 0 {
return fmt.Errorf("no accounts found")
}

if err := c.unlockAccount(accounts[0]); err != nil {
return fmt.Errorf("cannot unlock account. %v", err)
func checkShardsForProposal(c collatorClient, head *types.Header) error {
account, err := c.Account()
if err != nil {
return err
}

log.Info("Checking if we are an eligible collation proposer for a shard...")
for s := int64(0); s < shardCount; s++ {
// Checks if we are an eligible proposer according to the VMC
period := head.Number.Div(head.Number, big.NewInt(periodLength))
addr, err := c.vmc.VMCCaller.GetEligibleProposer(&bind.CallOpts{}, big.NewInt(s), period)
addr, err := c.VMCCaller().GetEligibleProposer(&bind.CallOpts{}, big.NewInt(s), period)
// TODO: When we are not a proposer, we get the error of being unable to
// unmarshal empty output. Open issue to deal with this.
if err != nil {
return err
}

// If output is non-empty and the addr == coinbase
if err == nil && addr == accounts[0].Address {
if addr == account.Address {
log.Info(fmt.Sprintf("Selected as collator on shard: %d", s))
err := proposeCollation(s)
if err != nil {
Expand Down
119 changes: 108 additions & 11 deletions sharding/collator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,125 @@ package sharding

import (
"context"
"fmt"
"errors"
"math/big"
"strings"
"testing"

"github.com/ethereum/go-ethereum"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/sharding/contracts"

"github.com/ethereum/go-ethereum/core/types"
)

type FakeClient struct {
client *FakeEthClient
type FakeCollatorClient struct {
accountAccount *accounts.Account
accountError error
chainReader FakeChainReader
contractCaller FakeContractCaller
}

func (c FakeCollatorClient) Account() (*accounts.Account, error) {
return c.accountAccount, c.accountError
}

type FakeEthClient struct{}
func (c FakeCollatorClient) ChainReader() ethereum.ChainReader {
return c.chainReader
}

func (c FakeCollatorClient) VMCCaller() *contracts.VMCCaller {
VMCCaller, err := contracts.NewVMCCaller(common.HexToAddress("0x0"), c.contractCaller)
if err != nil {
panic(err)
}
return VMCCaller
}

type FakeSubscription struct{}
type FakeChainReader struct {
subscribeNewHeadSubscription ethereum.Subscription
subscribeNewHeadError error
}

func (ec *FakeEthClient) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (FakeSubscription, error) {
return FakeSubscription{}, fmt.Errorf("error, network disconnected!")
func (r FakeChainReader) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) {
return r.subscribeNewHeadSubscription, r.subscribeNewHeadError
}
func (r FakeChainReader) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) {
return nil, nil
}
func (r FakeChainReader) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
return nil, nil
}
func (r FakeChainReader) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
return nil, nil
}
func (r FakeChainReader) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
return nil, nil
}
func (r FakeChainReader) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) {
return 0, nil
}
func (r FakeChainReader) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) {
return nil, nil
}

type FakeContractCaller struct {
codeAtBytes []byte
codeAtError error
callContractBytes []byte
callContractError error
}

func (c FakeContractCaller) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) {
return c.codeAtBytes, c.codeAtError
}

func (c FakeContractCaller) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
return c.callContractBytes, c.callContractError
}

func TestCheckShardsForProposal(t *testing.T) {
tests := []struct {
Name string
Head *types.Header
ExpectedPeriod *big.Int
ExpectedError string
CollatorClient FakeCollatorClient
}{
{
Name: "collatorClient.Account should return an error",
ExpectedError: "no account",
CollatorClient: FakeCollatorClient{
accountError: errors.New("no account"),
},
},
{
Name: "VMCCaller.GetEligibleProposer should return an error",
ExpectedError: "there is no cake",
CollatorClient: FakeCollatorClient{
accountAccount: &accounts.Account{},
contractCaller: FakeContractCaller{
callContractError: errors.New("there is no cake"),
},
},
Head: &types.Header{Number: big.NewInt(100)},
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
if err := checkShardsForProposal(tt.CollatorClient, tt.Head); !strings.Contains(safeError(err), tt.ExpectedError) {
t.Fatalf("Incorrect error! Wanted %v, got %v", tt.ExpectedError, err)
}
})
}
}

func TestSubscribeHeaders(t *testing.T) {
client := &FakeClient{client: &FakeEthClient{}}
err := subscribeBlockHeaders(client)
func safeError(err error) string {
if err != nil {
t.Errorf("subscribe new headers should work", "no error", err)
return err.Error()
}
return "nil"
}
9 changes: 0 additions & 9 deletions sharding/contracts/README.md

This file was deleted.

Loading

0 comments on commit 2ecf016

Please sign in to comment.