Skip to content
144 changes: 134 additions & 10 deletions pkg/chain/ethereum/beacon.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,154 @@
package ethereum

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

"github.com/ethereum/go-ethereum/common"
"github.com/keep-network/keep-common/pkg/chain/ethereum"
"github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
"github.com/keep-network/keep-core/pkg/chain"
"github.com/keep-network/keep-core/pkg/chain/random-beacon/gen/contract"
)

// Definitions of contract names.
const (
RandomBeaconContractName = "RandomBeacon"
)

// BeaconChain represents a beacon-specific chain handle.
type BeaconChain struct {
*Chain

randomBeacon *contract.RandomBeacon
sortitionPool *contract.SortitionPool
}

// NewBeaconChain construct a new instance of the beacon-specific Ethereum
// newBeaconChain construct a new instance of the beacon-specific Ethereum
// chain handle.
func NewBeaconChain(
ctx context.Context,
config *ethereum.Config,
client ethutil.EthereumClient,
func newBeaconChain(
config ethereum.Config,
baseChain *Chain,
) (*BeaconChain, error) {
chain, err := NewChain(ctx, config, client)
randomBeaconAddress, err := config.ContractAddress(RandomBeaconContractName)
if err != nil {
return nil, fmt.Errorf(
"failed to resolve %s contract address: [%v]",
RandomBeaconContractName,
err,
)
}

randomBeacon, err :=
contract.NewRandomBeacon(
randomBeaconAddress,
baseChain.chainID,
baseChain.key,
baseChain.client,
baseChain.nonceManager,
baseChain.miningWaiter,
baseChain.blockCounter,
baseChain.transactionMutex,
)
if err != nil {
return nil, fmt.Errorf(
"failed to attach to RandomBeacon contract: [%v]",
err,
)
}

sortitionPoolAddress, err := randomBeacon.SortitionPool()
if err != nil {
return nil, fmt.Errorf(
"failed to get sortition pool address: [%v]",
err,
)
}

sortitionPool, err :=
contract.NewSortitionPool(
sortitionPoolAddress,
baseChain.chainID,
baseChain.key,
baseChain.client,
baseChain.nonceManager,
baseChain.miningWaiter,
baseChain.blockCounter,
baseChain.transactionMutex,
)
if err != nil {
return nil, fmt.Errorf("cannot create base chain handle: [%v]", err)
return nil, fmt.Errorf(
"failed to attach to SortitionPool contract: [%v]",
err,
)
}

return &BeaconChain{
Chain: chain,
Chain: baseChain,
randomBeacon: randomBeacon,
sortitionPool: sortitionPool,
}, nil
}

// OperatorToStakingProvider returns the staking provider address for the
// current operator. If the staking provider has not been registered for the
// operator, the returned address is empty and the boolean flag is set to false
// If the staking provider has been registered, the address is not empty and the
// boolean flag indicates true.
func (bc *BeaconChain) OperatorToStakingProvider() (chain.Address, bool, error) {
stakingProvider, err := bc.randomBeacon.OperatorToStakingProvider(bc.key.Address)
if err != nil {
return "", false, fmt.Errorf(
"failed to map operator %v to a staking provider: [%v]",
bc.key.Address,
err,
)
}

if bytes.Equal(
stakingProvider.Bytes(),
bytes.Repeat([]byte{0}, common.AddressLength),
) {
return "", false, nil
}

return chain.Address(stakingProvider.Hex()), true, nil
}

// EligibleStake returns the current value of the staking provider's eligible
// stake. Eligible stake is defined as the currently authorized stake minus the
// pending authorization decrease. Eligible stake is what is used for operator's
// weight in the sortition pool. If the authorized stake minus the pending
// authorization decrease is below the minimum authorization, eligible stake
// is 0.
func (bc *BeaconChain) EligibleStake(stakingProvider chain.Address) (*big.Int, error) {
eligibleStake, err := bc.randomBeacon.EligibleStake(common.HexToAddress(stakingProvider.String()))
if err != nil {
return nil, fmt.Errorf(
"failed to get eligible stake for staking provider %s: [%w]",
stakingProvider,
err,
)
}

return eligibleStake, nil
}

// IsPoolLocked returns true if the sortition pool is locked and no state
// changes are allowed.
func (bc *BeaconChain) IsPoolLocked() (bool, error) {
return bc.sortitionPool.IsLocked()
}

// IsOperatorInPool returns true if the current operator is registered in the
// sortition pool.
func (bc *BeaconChain) IsOperatorInPool() (bool, error) {
return bc.randomBeacon.IsOperatorInPool(bc.key.Address)
}

// JoinSortitionPool executes a transaction to have the current operator join
// the sortition pool.
func (bc *BeaconChain) JoinSortitionPool() error {
_, err := bc.randomBeacon.JoinSortitionPool()
return err
}
95 changes: 84 additions & 11 deletions pkg/chain/ethereum/ethereum.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package ethereum
import (
"context"
"fmt"
"math/big"
"sync"

"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/keep-network/keep-common/pkg/chain/ethlike"

"github.com/ipfs/go-log"
Expand All @@ -18,17 +22,75 @@ var logger = log.Logger("keep-chain-ethereum")
// provides the implementation of generic features like balance monitor,
// block counter and similar.
type Chain struct {
key *keystore.Key
client ethutil.EthereumClient
key *keystore.Key
client ethutil.EthereumClient
chainID *big.Int

blockCounter *ethlike.BlockCounter
nonceManager *ethlike.NonceManager
miningWaiter *ethutil.MiningWaiter

// transactionMutex allows interested parties to forcibly serialize
// transaction submission.
//
// When transactions are submitted, they require a valid nonce. The nonce is
// equal to the count of transactions the account has submitted so far, and
// for a transaction to be accepted it should be monotonically greater than
// any previous submitted transaction. To do this, transaction submission
// asks the Ethereum client it is connected to for the next pending nonce,
// and uses that value for the transaction. Unfortunately, if multiple
// transactions are submitted in short order, they may all get the same
// nonce. Serializing submission ensures that each nonce is requested after
// a previous transaction has been submitted.
transactionMutex *sync.Mutex
}

// NewChain construct a new instance of the Ethereum chain handle.
func NewChain(
// Connect creates Random Beacon and TBTC Ethereum chain handles.
func Connect(
ctx context.Context,
config *ethereum.Config,
client ethutil.EthereumClient,
config ethereum.Config,
) (*BeaconChain, *TbtcChain, error) {
client, err := ethclient.Dial(config.URL)
if err != nil {
return nil, nil, fmt.Errorf(
"error Connecting to Ethereum Server: %s [%v]",
config.URL,
err,
)
}

baseChain, err := newChain(ctx, config, client)
if err != nil {
return nil, nil, fmt.Errorf("could not create base chain handle: [%v]", err)
}

beaconChain, err := newBeaconChain(config, baseChain)
if err != nil {
return nil, nil, fmt.Errorf("could not create beacon chain handle: [%v]", err)
}

tbtcChain, err := newTbtcChain(baseChain)
if err != nil {
return nil, nil, fmt.Errorf("could not create TBTC chain handle: [%v]", err)
}

return beaconChain, tbtcChain, nil
}

// newChain construct a new instance of the Ethereum chain handle.
func newChain(
ctx context.Context,
config ethereum.Config,
client *ethclient.Client,
) (*Chain, error) {
chainID, err := client.ChainID(ctx)
if err != nil {
return nil, fmt.Errorf(
"failed to resolve Ethereum chain id: [%v]",
err,
)
}

key, err := decryptKey(config)
if err != nil {
return nil, fmt.Errorf(
Expand All @@ -47,19 +109,30 @@ func NewChain(
)
}

nonceManager := ethutil.NewNonceManager(
clientWithAddons,
key.Address,
)

miningWaiter := ethutil.NewMiningWaiter(clientWithAddons, config)

// TODO: Consider adding the balance monitoring.

return &Chain{
key: key,
client: clientWithAddons,
blockCounter: blockCounter,
key: key,
client: clientWithAddons,
chainID: chainID,
blockCounter: blockCounter,
nonceManager: nonceManager,
miningWaiter: miningWaiter,
transactionMutex: &sync.Mutex{},
}, nil
}

// wrapClientAddons wraps the client instance with add-ons like logging, rate
// limiting and so on.
func wrapClientAddons(
config *ethereum.Config,
config ethereum.Config,
client ethutil.EthereumClient,
) ethutil.EthereumClient {
loggingClient := ethutil.WrapCallLogging(logger, client)
Expand All @@ -86,7 +159,7 @@ func wrapClientAddons(
}

// decryptKey decrypts the chain key pointed by the config.
func decryptKey(config *ethereum.Config) (*keystore.Key, error) {
func decryptKey(config ethereum.Config) (*keystore.Key, error) {
return ethutil.DecryptKeyFile(
config.Account.KeyFile,
config.Account.KeyFilePassword,
Expand Down
19 changes: 3 additions & 16 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
package ethereum

import (
"context"
"fmt"
"github.com/keep-network/keep-common/pkg/chain/ethereum"
"github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
)

// TbtcChain represents a TBTC-specific chain handle.
type TbtcChain struct {
*Chain
}

// NewTbtcChain construct a new instance of the TBTC-specific Ethereum
// chain handle.
func NewTbtcChain(
ctx context.Context,
config *ethereum.Config,
client ethutil.EthereumClient,
func newTbtcChain(
baseChain *Chain,
) (*TbtcChain, error) {
chain, err := NewChain(ctx, config, client)
if err != nil {
return nil, fmt.Errorf("cannot create base chain handle: [%v]", err)
}

return &TbtcChain{
Chain: chain,
Chain: baseChain,
}, nil
}
21 changes: 21 additions & 0 deletions pkg/sortition/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,30 @@ import (

// Chain handle for interaction with the sortition pool contracts.
type Chain interface {
// OperatorToStakingProvider returns the staking provider address for the
// current operator. If the staking provider has not been registered for the
// operator, the returned address is empty and the boolean flag is set to
// false. If the staking provider has been registered, the address is not
// empty and the boolean flag indicates true.
OperatorToStakingProvider() (chain.Address, bool, error)

// EligibleStake returns the current value of the staking provider's
// eligible stake. Eligible stake is defined as the currently authorized
// stake minus the pending authorization decrease. Eligible stake
// is what is used for operator's weight in the sortition pool.
// If the authorized stake minus the pending authorization decrease
// is below the minimum authorization, eligible stake is 0.
EligibleStake(stakingProvider chain.Address) (*big.Int, error)

// IsPoolLocked returns true if the sortition pool is locked and no state
// changes are allowed.
IsPoolLocked() (bool, error)

// IsOperatorInPool returns true if the current operator is registered in
// the sortition pool.
IsOperatorInPool() (bool, error)

// JoinSortitionPool executes a transaction to have the current operator
// join the sortition pool.
JoinSortitionPool() error
}