diff --git a/config/config.go b/config/config.go index 1b8c9b9..8f60872 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,8 @@ package config import ( + "fmt" + "github.com/spf13/viper" ) @@ -13,6 +15,7 @@ type Config struct { Ethereum EthereumConfig `mapstructure:"ethereum"` Filecoin FilecoinConfig `mapstructure:"filecoin"` PubSub PubSubConfig `mapstructure:"pubSub"` + Store StoreConfig `mapstructure:"store"` } type EthereumConfig struct { @@ -31,8 +34,17 @@ type PubSubConfig struct { ProtocolID string `mapstructure:"protocolID"` } +type StoreConfig struct { + DatabaseURL string `mapstructure:"database_url"` +} + // NewConfig creates a new config based on default values or provided .env file func NewConfig(configPath string) (*Config, error) { + dbName := "dione" + username := "user" + password := "password" + dbURL := fmt.Sprintf("host=localhost user=%s password=%s dbname=%s sslmode=disable", username, password, dbName) + cfg := &Config{ ListenAddr: "localhost", ListenPort: ":8000", @@ -45,6 +57,9 @@ func NewConfig(configPath string) (*Config, error) { PubSub: PubSubConfig{ ProtocolID: "p2p-oracle", }, + Store: StoreConfig{ + DatabaseURL: dbURL, + }, } viper.SetConfigFile(configPath) diff --git a/consensus/leader.go b/consensus/leader.go index 73ba3d9..97aff23 100644 --- a/consensus/leader.go +++ b/consensus/leader.go @@ -13,18 +13,6 @@ import ( "golang.org/x/xerrors" ) -type MinerWallet interface { - WalletSign(context.Context, address.Address, []byte) (*crypto.Signature, error) -} - -type MinerBase struct { - MinerStake types.BigInt - NetworkStake types.BigInt - WorkerKey address.Address - PrevBeaconEntry types.BeaconEntry - BeaconEntries []types.BeaconEntry -} - type SignFunc func(context.Context, address.Address, []byte) (*crypto.Signature, error) func ComputeVRF(ctx context.Context, sign SignFunc, worker address.Address, sigInput []byte) ([]byte, error) { @@ -57,7 +45,7 @@ func VerifyVRF(ctx context.Context, worker address.Address, vrfBase, vrfproof [] } func IsRoundWinner(ctx context.Context, round types.TaskEpoch, - worker address.Address, brand types.BeaconEntry, mbi *MinerBase, a MinerWallet) (*types.ElectionProof, error) { + worker address.Address, brand types.BeaconEntry, mb *MinerBase, a MinerAPI) (*types.ElectionProof, error) { buf := new(bytes.Buffer) if err := worker.MarshalCBOR(buf); err != nil { @@ -69,13 +57,13 @@ func IsRoundWinner(ctx context.Context, round types.TaskEpoch, return nil, xerrors.Errorf("failed to draw randomness: %w", err) } - vrfout, err := ComputeVRF(ctx, a.WalletSign, mbi.WorkerKey, electionRand) + vrfout, err := ComputeVRF(ctx, a.WalletSign, mb.WorkerKey, electionRand) if err != nil { return nil, xerrors.Errorf("failed to compute VRF: %w", err) } ep := &types.ElectionProof{VRFProof: vrfout} - j := ep.ComputeWinCount(mbi.MinerStake, mbi.NetworkStake) + j := ep.ComputeWinCount(mb.MinerStake, mb.NetworkStake) ep.WinCount = j if j < 1 { return nil, nil diff --git a/consensus/miner.go b/consensus/miner.go new file mode 100644 index 0000000..448a2df --- /dev/null +++ b/consensus/miner.go @@ -0,0 +1,110 @@ +package consensus + +import ( + "bytes" + "context" + + "github.com/Secured-Finance/dione/types" + "github.com/ethereum/go-ethereum/common" + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/crypto" + "github.com/sirupsen/logrus" + "golang.org/x/xerrors" +) + +type Miner struct { + address address.Address + api MinerAPI +} + +type MinerAPI interface { + WalletSign(context.Context, address.Address, []byte) (*crypto.Signature, error) + // TODO: get miner base based on epoch; +} + +type MinerBase struct { + MinerStake types.BigInt + NetworkStake types.BigInt + WorkerKey address.Address + EthWallet common.Address + PrevBeaconEntry types.BeaconEntry + BeaconEntries []types.BeaconEntry + NullRounds types.TaskEpoch +} + +type MiningBase struct { + epoch types.TaskEpoch + nullRounds types.TaskEpoch +} + +func NewMinerBase(minerStake, networkStake types.BigInt, minerWallet address.Address, + minerEthWallet common.Address, prev types.BeaconEntry, entries []types.BeaconEntry) *MinerBase { + return &MinerBase{ + MinerStake: minerStake, + NetworkStake: networkStake, + WorkerKey: minerWallet, + EthWallet: minerEthWallet, + PrevBeaconEntry: prev, + BeaconEntries: entries, + } +} + +func NewMiningBase() *MiningBase { + return &MiningBase{ + nullRounds: 0, + } +} + +// Start, Stop mining functions + +func (m *Miner) MineTask(ctx context.Context, base *MiningBase, mb *MinerBase) (*types.DioneTask, error) { + round := base.epoch + base.nullRounds + 1 + logrus.Debug("attempting to mine the task at epoch: ", round) + + prevEntry := mb.PrevBeaconEntry + + ticket, err := m.computeTicket(ctx, &prevEntry, base, mb) + if err != nil { + return nil, xerrors.Errorf("scratching ticket failed: %w", err) + } + + winner, err := IsRoundWinner(ctx, round, m.address, prevEntry, mb, m.api) + if err != nil { + return nil, xerrors.Errorf("failed to check if we win next round: %w", err) + } + + if winner == nil { + return nil, nil + } + return &types.DioneTask{ + Miner: m.address, + Ticket: ticket, + ElectionProof: winner, + BeaconEntries: mb.BeaconEntries, + // TODO: signature + Height: round, + }, nil +} + +func (m *Miner) computeTicket(ctx context.Context, brand *types.BeaconEntry, base *MiningBase, mb *MinerBase) (*types.Ticket, error) { + buf := new(bytes.Buffer) + if err := m.address.MarshalCBOR(buf); err != nil { + return nil, xerrors.Errorf("failed to marshal address to cbor: %w", err) + } + + round := base.epoch + base.nullRounds + 1 + + input, err := DrawRandomness(brand.Data, crypto.DomainSeparationTag_TicketProduction, round-types.TicketRandomnessLookback, buf.Bytes()) + if err != nil { + return nil, err + } + + vrfOut, err := ComputeVRF(ctx, m.api.WalletSign, mb.WorkerKey, input) + if err != nil { + return nil, err + } + + return &types.Ticket{ + VRFProof: vrfOut, + }, nil +} diff --git a/go.mod b/go.mod index 08aa1b9..fd1ddbf 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/google/uuid v1.1.1 github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect + github.com/jmoiron/sqlx v1.2.0 github.com/karalabe/usb v0.0.0-20191104083709-911d15fe12a9 // indirect github.com/libp2p/go-libp2p v0.11.0 github.com/libp2p/go-libp2p-core v0.6.1 diff --git a/go.sum b/go.sum index 2bfc989..4ae4f68 100644 --- a/go.sum +++ b/go.sum @@ -680,6 +680,8 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -730,6 +732,7 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-addr-util v0.0.1/go.mod h1:4ac6O7n9rIAKB1dnd+s8IbbMXkt+oBpzX4/+RACcnlQ= github.com/libp2p/go-addr-util v0.0.2 h1:7cWK5cdA5x72jX0g8iLrQWm5TRJZ6CzGdPEhWj7plWU= @@ -1075,6 +1078,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-xmlrpc v0.0.3/go.mod h1:mqc2dz7tP5x5BKlCahN/n+hs7OSZKJkS9JsHNBRlrxA= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= diff --git a/rpcclient/ethereum.go b/rpcclient/ethereum.go index 48be19f..ca2ff5e 100644 --- a/rpcclient/ethereum.go +++ b/rpcclient/ethereum.go @@ -17,6 +17,7 @@ import ( // TODO: change artifacts for other contracts type EthereumClient struct { client *ethclient.Client + ethAddress *common.Address authTransactor *bind.TransactOpts oracleEmitter *oracleemitter.OracleEmitterSession aggregator *aggregator.AggregatorSession @@ -53,6 +54,7 @@ func (c *EthereumClient) Initialize(ctx context.Context, url, privateKey, oracle if err != nil { return err } + c.ethAddress = &c.authTransactor.From authTransactor := bind.NewKeyedTransactor(ecdsaKey) c.authTransactor = authTransactor @@ -158,26 +160,6 @@ func (c *EthereumClient) Initialize(ctx context.Context, url, privateKey, oracle // return TxHash // } -// func (c *EthereumClient) GenerateAddressFromPrivateKey(private_key string) string { -// privateKey, err := crypto.HexToECDSA(private_key) -// if err != nil { -// c.Logger.Fatal("Failed to generate private key", err) -// } - -// publicKey := privateKey.Public() -// publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) -// if !ok { -// c.Logger.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey") -// } - -// publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) -// c.Logger.Info(hexutil.Encode(publicKeyBytes)[4:]) - -// address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() - -// return address -// } - func (c *EthereumClient) SubscribeOnOracleEvents(incomingEventsChan chan *oracleemitter.OracleEmitterNewOracleRequest) (event.Subscription, error) { requestsFilter := c.oracleEmitter.Contract.OracleEmitterFilterer subscription, err := requestsFilter.WatchNewOracleRequest(&bind.WatchOpts{ @@ -224,6 +206,9 @@ func (c *EthereumClient) SubmitRequestAnswer(reqID *big.Int, data string, callba return nil } +// Getting total stake in DioneStaking contract, this function could +// be used for storing the total stake and veryfing the stake tokens +// on new tasks func (c *EthereumClient) GetTotalStake() (*big.Int, error) { totalStake, err := c.dioneStaking.TotalStake() if err != nil { @@ -232,6 +217,9 @@ func (c *EthereumClient) GetTotalStake() (*big.Int, error) { return totalStake, nil } +// Getting miner stake in DioneStaking contract, this function could +// be used for storing the miner's stake and veryfing the stake tokens +// on new tasks func (c *EthereumClient) GetMinerStake(minerAddress common.Address) (*big.Int, error) { minerStake, err := c.dioneStaking.MinerStake(minerAddress) if err != nil { diff --git a/rpcclient/wallet.go b/rpcclient/wallet.go new file mode 100644 index 0000000..d0032ad --- /dev/null +++ b/rpcclient/wallet.go @@ -0,0 +1,35 @@ +package rpcclient + +import ( + "crypto/ecdsa" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/sirupsen/logrus" +) + +func GenerateEthWalletAddressFromPrivateKey(private_key string) common.Address { + privateKey, err := crypto.HexToECDSA(private_key) + if err != nil { + logrus.Fatal("Failed to generate private key", err) + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + logrus.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey") + } + + publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) + logrus.Info(hexutil.Encode(publicKeyBytes)[4:]) + + address := crypto.PubkeyToAddress(*publicKeyECDSA) + + return address +} + +// Convert common.Address type into string for ethereum wallet +func EthWalletToString(ethWallet common.Address) string { + return ethWallet.Hex() +} diff --git a/store/stake.go b/store/stake.go index a574ac7..a00211a 100644 --- a/store/stake.go +++ b/store/stake.go @@ -2,28 +2,37 @@ package store import ( "math/big" + "time" + "github.com/Secured-Finance/dione/lib" "github.com/Secured-Finance/dione/rpcclient" + "github.com/Secured-Finance/dione/types" "github.com/ethereum/go-ethereum/common" + validation "github.com/go-ozzo/ozzo-validation" ) -// TODO: specify store for staking mechanism type DioneStakeInfo struct { - MinerStake *big.Int - TotalStake *big.Int - Ethereum *rpcclient.EthereumClient + ID int + MinerStake *big.Int + TotalStake *big.Int + MinerWallet string + MinerEthWallet string + Timestamp time.Time + Ethereum *rpcclient.EthereumClient } -func NewDioneStakeInfo(minerStake, totalStake *big.Int, ethereumClient *rpcclient.EthereumClient) *DioneStakeInfo { +func NewDioneStakeInfo(minerStake, totalStake *big.Int, minerWallet, minerEthWallet string, ethereumClient *rpcclient.EthereumClient) *DioneStakeInfo { return &DioneStakeInfo{ - MinerStake: minerStake, - TotalStake: totalStake, - Ethereum: ethereumClient, + MinerStake: minerStake, + TotalStake: totalStake, + MinerWallet: minerWallet, + MinerEthWallet: minerEthWallet, + Ethereum: ethereumClient, } } -func (d *DioneStakeInfo) UpdateMinerStake(minerAddress common.Address) error { - minerStake, err := d.Ethereum.GetMinerStake(minerAddress) +func (d *DioneStakeInfo) UpdateMinerStake(minerEthAddress common.Address) error { + minerStake, err := d.Ethereum.GetMinerStake(minerEthAddress) if err != nil { return err } @@ -33,7 +42,7 @@ func (d *DioneStakeInfo) UpdateMinerStake(minerAddress common.Address) error { return nil } -func (d *DioneStakeInfo) UpdateTotalStake(minerAddress common.Address) error { +func (d *DioneStakeInfo) UpdateTotalStake() error { totalStake, err := d.Ethereum.GetTotalStake() if err != nil { return err @@ -43,3 +52,46 @@ func (d *DioneStakeInfo) UpdateTotalStake(minerAddress common.Address) error { return nil } + +// Put miner's staking information into the database +func (s *Store) CreateDioneStakeInfo(stakeStore *DioneStakeInfo) error { + + if err := stakeStore.Validate(); err != nil { + return err + } + + now := lib.Clock.Now() + + return s.db.QueryRow( + "INSERT INTO staking (miner_stake, total_stake, miner_wallet, miner_eth_wallet, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING id", + stakeStore.MinerStake, + stakeStore.TotalStake, + stakeStore.MinerWallet, + stakeStore.MinerEthWallet, + now, + ).Scan(&stakeStore.ID) +} + +func (s *Store) GetLastStakeInfo(wallet, ethWallet string) (*DioneStakeInfo, error) { + var stake *DioneStakeInfo + if err := s.db.Select(&stake, + `SELECT miner_stake, total_stake, miner_wallet, miner_eth_wallet, timestamp FROM staking ORDER BY TIMESTAMP DESC LIMIT 1 WHERE miner_wallet=$1, miner_eth_wallet=$2`, + wallet, + ethWallet, + ); err != nil { + return nil, err + } + + return stake, nil +} + +// Before puting the data into the database validating all required fields +func (s *DioneStakeInfo) Validate() error { + return validation.ValidateStruct( + s, + validation.Field(&s.MinerStake, validation.Required, validation.By(types.ValidateBigInt(s.MinerStake))), + validation.Field(&s.TotalStake, validation.Required, validation.By(types.ValidateBigInt(s.TotalStake))), + validation.Field(&s.MinerWallet, validation.Required), + validation.Field(&s.MinerEthWallet, validation.Required), + ) +} diff --git a/store/store.go b/store/store.go index 78be46f..8c1bf8a 100644 --- a/store/store.go +++ b/store/store.go @@ -1,24 +1,45 @@ package store import ( - "database/sql" - "github.com/Secured-Finance/dione/node" + "github.com/jmoiron/sqlx" ) type Store struct { - db *sql.DB - node *node.Node - genesisTs uint64 + db *sqlx.DB + node *node.Node + genesisTs uint64 + StakeStorage DioneStakeInfo // genesisTask *types.DioneTask } -func NewStore(db *sql.DB, node *node.Node, genesisTs uint64) *Store { +func NewStore(node *node.Node, genesisTs uint64) (*Store, error) { + db, err := newDB(node.Config.Store.DatabaseURL) + if err != nil { + return nil, err + } + + defer db.Close() + return &Store{ db: db, node: node, genesisTs: genesisTs, - } + }, nil } -// TODO: connect data base; specify the table for stake storage; queries for stake storage +func newDB(databaseURL string) (*sqlx.DB, error) { + db, err := sqlx.Connect("postgres", databaseURL) + if err != nil { + return nil, err + } + + if err := db.Ping(); err != nil { + return nil, err + } + + return db, nil +} + +// TODO: Discuss with ChronosX88 about using custom database to decrease I/O bound +// specify the migrations for stake storage; diff --git a/types/bigint.go b/types/bigint.go index 9190abf..642ecd7 100644 --- a/types/bigint.go +++ b/types/bigint.go @@ -1,9 +1,11 @@ package types import ( + "errors" "math/big" big2 "github.com/filecoin-project/go-state-types/big" + validation "github.com/go-ozzo/ozzo-validation" ) var EmptyInt = BigInt{} @@ -18,3 +20,13 @@ func BigFromBytes(b []byte) BigInt { i := big.NewInt(0).SetBytes(b) return BigInt{Int: i} } + +func ValidateBigInt(i *big.Int) validation.RuleFunc { + return func(value interface{}) error { + bigInt := i.IsInt64() + if !bigInt { + return errors.New("expected big integer") + } + return nil + } +} diff --git a/types/task.go b/types/task.go index c78beb3..49bf027 100644 --- a/types/task.go +++ b/types/task.go @@ -11,6 +11,8 @@ import ( // TaskEpoch represents the timestamp of Task computed by the Dione miner type TaskEpoch int64 +const TicketRandomnessLookback = TaskEpoch(1) + func (e TaskEpoch) String() string { return strconv.FormatInt(int64(e), 10) }