diff --git a/config/activations.go b/config/activations.go index 73cd837..6ed917c 100644 --- a/config/activations.go +++ b/config/activations.go @@ -77,6 +77,12 @@ var ( // // Activation of 2.0.5 PIP10AverageActivation uint32 = 295190 + + // PIP18DelegateStakingActivation implements delegate staking by using PEG addresses. + // 1. Balances of PEG for each address is more complicated. It is the balance of PEG for the address (assuming it has not be delegated) + // 2. We quit looking at the rich list, and just consider the top 100 submissions with the highest stake + // 3. We pay out with the ratio of the total PEG staked. (Removes old top 100 PEG addresses staking reward and give staking opportunity to all PEG holders) + PIP18DelegateStakingActivation uint32 = 314482 ) func SetAllActivations(act uint32) { @@ -97,4 +103,5 @@ func SetAllActivations(act uint32) { V204EnhanceActivation = act V204BurnMintedTokenActivation = act PIP10AverageActivation = act + PIP18DelegateStakingActivation = act } diff --git a/node/pegnet/addresses.go b/node/pegnet/addresses.go index 16ca618..7af0e8a 100644 --- a/node/pegnet/addresses.go +++ b/node/pegnet/addresses.go @@ -462,6 +462,25 @@ func (p *Pegnet) IsIncludedTopPEGAddress(address []byte) bool { return false } +func (p *Pegnet) GetPEGAddress(address []byte) (uint64, error) { + stmt2 := `SELECT peg_balance FROM pn_addresses WHERE address = ?;` + rows, err2 := p.DB.Query(stmt2, address) + if err2 != nil { + fmt.Println("DB query is failed") + return 0, err2 + } + defer rows.Close() + + for rows.Next() { + var pegBalance uint64 + if err := rows.Scan(&pegBalance); err != nil { + return 0, err + } + return pegBalance, nil + } + return 0, nil +} + // SelectRichList returns the balance of all addresses for a given ticker func (p *Pegnet) SelectRichList(ticker fat2.PTicker, count int) ([]BalancePair, error) { if ticker <= fat2.PTickerInvalid || fat2.PTickerMax <= ticker { diff --git a/node/pegnet/txhistory.go b/node/pegnet/txhistory.go index 82074ed..4f76685 100644 --- a/node/pegnet/txhistory.go +++ b/node/pegnet/txhistory.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/pegnet/pegnet/modules/graderDelegateStake" "strings" "time" @@ -426,6 +427,45 @@ func (p *Pegnet) InsertStaking100Coinbase(tx *sql.Tx, winner *graderStake.Gradin return nil } +// InsertCoinbase inserts the payouts from staking into the history system. +// There is one transaction per winning SPR, with the entry hash pointing to that specific spr +func (p *Pegnet) InsertStaking100CoinbaseDelegate(tx *sql.Tx, winner *graderDelegateStake.GradingDelegatedSPR, addr []byte, timestamp time.Time) error { + stmt, err := tx.Prepare(`INSERT INTO "pn_history_txbatch" + (entry_hash, height, blockorder, timestamp, executed) VALUES + (?, ?, ?, ?, ?)`) + if err != nil { + return err + } + + lookup, err := tx.Prepare(insertLookupQuery) + if err != nil { + return err + } + + _, err = stmt.Exec(winner.EntryHash, winner.SPR.GetHeight(), 0, timestamp.Unix(), winner.SPR.GetHeight()) + if err != nil { + return err + } + + coinbaseStatement, err := tx.Prepare(`INSERT INTO "pn_history_transaction" + (entry_hash, tx_index, action_type, from_address, from_asset, from_amount, to_asset, to_amount, outputs) VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?)`) + if err != nil { + return err + } + + _, err = coinbaseStatement.Exec(winner.EntryHash, 0, Coinbase, addr, "", 0, "PEG", winner.Payout(), "") + if err != nil { + return err + } + + if _, err = lookup.Exec(winner.EntryHash, 0, addr); err != nil { + return err + } + + return nil +} + // InsertStakingCoinbase inserts the payouts from mining into the history system. // There is one transaction per winning OPR, with the entry hash pointing to that specific opr func (p *Pegnet) InsertStakingCoinbase(tx *sql.Tx, txid string, height uint32, heightTimestamp time.Time, payouts map[string]uint64, addressMap map[string]factom.FAAddress) error { @@ -614,4 +654,4 @@ func (p *Pegnet) InsertZeroingCoinbase(tx *sql.Tx, txid string, addTxid string, } return nil -} \ No newline at end of file +} diff --git a/node/spr.go b/node/spr.go index 5c11c49..1df3bf1 100644 --- a/node/spr.go +++ b/node/spr.go @@ -3,8 +3,10 @@ package node import ( "context" "fmt" + "github.com/pegnet/pegnet/modules/spr" "github.com/Factom-Asset-Tokens/factom" + "github.com/pegnet/pegnet/modules/graderDelegateStake" "github.com/pegnet/pegnet/modules/graderStake" "github.com/pegnet/pegnetd/config" ) @@ -31,27 +33,101 @@ func (d *Pegnetd) GradeS(ctx context.Context, block *factom.EBlock) (graderStake if block.Height >= config.V202EnhanceActivation { ver = 7 } + if block.Height >= config.PIP18DelegateStakingActivation { + ver = 8 + } + + if ver < 8 { + g, err := graderStake.NewGrader(ver, int32(block.Height)) + if err != nil { + return nil, err + } + for _, entry := range block.Entries { + extids := make([][]byte, len(entry.ExtIDs)) + for i := range entry.ExtIDs { + extids[i] = entry.ExtIDs[i] + } + // allow only top 100 stake holders submit prices + stakerRCD := extids[1] + if d.Pegnet.IsIncludedTopPEGAddress(stakerRCD) { + // ignore bad opr errors + err = g.AddSPR(entry.Hash[:], extids, entry.Content) + if err != nil { + // This is a noisy debug print + //logrus.WithError(err).WithFields(logrus.Fields{"hash": entry.Hash.String()}).Debug("failed to add spr") + } + } + } + return g.Grade(), nil + } + return nil, nil +} + +func isElementExist(element string, list []string) bool { + isExist := false + for j := 0; j < len(list); j++ { + if list[j] == element { + isExist = true + } + } + return isExist +} + +// Grade Staking Price Records +func (d *Pegnetd) GradeDelegatedS(ctx context.Context, block *factom.EBlock) (graderDelegateStake.DelegatedGradedBlock, error) { + if block == nil { + // TODO: Handle the case where there is no opr block. + // Must delay conversions if this- happens + return nil, nil + } + + if *block.ChainID != config.SPRChain { + return nil, fmt.Errorf("trying to grade a non-spr chain") + } - g, err := graderStake.NewGrader(ver, int32(block.Height)) - if err != nil { - return nil, err + ver := uint8(8) + if block.Height >= config.PIP18DelegateStakingActivation { + ver = 8 } - for _, entry := range block.Entries { - extids := make([][]byte, len(entry.ExtIDs)) - for i := range entry.ExtIDs { - extids[i] = entry.ExtIDs[i] + + if ver == 8 { + g, err := graderDelegateStake.NewDelegatedGrader(ver, int32(block.Height)) + if err != nil { + return nil, err } - // allow only top 100 stake holders submit prices - stakerRCD := extids[1] - if d.Pegnet.IsIncludedTopPEGAddress(stakerRCD) { - // ignore bad opr errors - err = g.AddSPR(entry.Hash[:], extids, entry.Content) + var groupOfDelegatorsAddress []string + for _, entry := range block.Entries { + extids := make([][]byte, len(entry.ExtIDs)) + for i := range entry.ExtIDs { + extids[i] = entry.ExtIDs[i] + } + o2, errP := spr.ParseS1Content(entry.Content) + var balanceOfPEG uint64 = 0 + if errP == nil { + balanceOfPEG, _ = d.Pegnet.GetPEGAddress([]byte(o2.Address)) + } + if errP == nil && len(extids) == 5 && len(extids[0]) == 1 && extids[0][0] == 8 { + listOfDelegatorsAddress, err := g.GetDelegatorsAddress(extids[3], extids[4], o2.Address) + if err != nil { + continue + } + for i := 0; i < len(listOfDelegatorsAddress); i++ { + isDuplicatedAddress := isElementExist(listOfDelegatorsAddress[i], groupOfDelegatorsAddress) + if isDuplicatedAddress { + continue + } + individualBalance, _ := d.Pegnet.GetPEGAddress([]byte(listOfDelegatorsAddress[i])) + balanceOfPEG += individualBalance + groupOfDelegatorsAddress = append(groupOfDelegatorsAddress, listOfDelegatorsAddress[i]) + } + } + err = g.AddSPRV4(entry.Hash[:], extids, entry.Content, balanceOfPEG) if err != nil { // This is a noisy debug print //logrus.WithError(err).WithFields(logrus.Fields{"hash": entry.Hash.String()}).Debug("failed to add spr") } } + return g.Grade(), nil } - - return g.Grade(), nil + return nil, nil } diff --git a/node/sync.go b/node/sync.go index dcac58a..9a4e25a 100644 --- a/node/sync.go +++ b/node/sync.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/pegnet/pegnet/modules/graderDelegateStake" "math/big" "sort" "time" @@ -380,7 +381,17 @@ func (d *Pegnetd) SyncBlock(ctx context.Context, tx *sql.Tx, height uint32) erro // Then, grade the new OPR Block. The results of this will be used // to execute conversions that are in holding. gradedBlock, err := d.Grade(ctx, oprEBlock) - gradedSPRBlock, err_s := d.GradeS(ctx, sprEBlock) + + var gradedSPRBlock graderStake.GradedBlock + var gradedDelegatedSPRBlock graderDelegateStake.DelegatedGradedBlock + var err_s error + if sprEBlock != nil { + if height < config.PIP18DelegateStakingActivation { + gradedSPRBlock, err_s = d.GradeS(ctx, sprEBlock) + } else { + gradedDelegatedSPRBlock, err_s = d.GradeDelegatedS(ctx, sprEBlock) + } + } isRatesAvailable := false if height < config.V20HeightActivation { if err != nil { @@ -441,10 +452,19 @@ func (d *Pegnetd) SyncBlock(ctx context.Context, tx *sql.Tx, height uint32) erro oprWinners = winnersOpr[0].OPR.GetOrderedAssetsUint() } } - if gradedSPRBlock != nil { - winnersSpr := gradedSPRBlock.Winners() - if 0 < len(winnersSpr) { - sprWinners = winnersSpr[0].SPR.GetOrderedAssetsUint() + if height < config.PIP18DelegateStakingActivation { + if gradedSPRBlock != nil { + winnersSpr := gradedSPRBlock.Winners() + if 0 < len(winnersSpr) { + sprWinners = winnersSpr[0].SPR.GetOrderedAssetsUint() + } + } + } else { + if gradedDelegatedSPRBlock != nil { + winnersSpr := gradedDelegatedSPRBlock.Winners() + if 0 < len(winnersSpr) { + sprWinners = winnersSpr[0].SPR.GetOrderedAssetsUint() + } } } if 0 < len(oprWinners) || 0 < len(sprWinners) { @@ -555,9 +575,17 @@ func (d *Pegnetd) SyncBlock(ctx context.Context, tx *sql.Tx, height uint32) erro if height >= config.V20HeightActivation { // 5) Apply effects of graded SPR Block (PEG rewards, if any) // These funds will be available for transactions and conversions executed in the next block - if gradedSPRBlock != nil { - if err := d.ApplyGradedSPRBlock(tx, gradedSPRBlock, dblock.Timestamp); err != nil { - return err + if height < config.PIP18DelegateStakingActivation { + if gradedSPRBlock != nil { + if err := d.ApplyGradedSPRBlock(tx, gradedSPRBlock, dblock.Timestamp); err != nil { + return err + } + } + } else { + if gradedDelegatedSPRBlock != nil { + if err := d.ApplyGradedDelegatedSPRBlock(tx, gradedDelegatedSPRBlock, dblock.Timestamp); err != nil { + return err + } } } } @@ -1421,6 +1449,34 @@ func (d *Pegnetd) ApplyGradedSPRBlock(tx *sql.Tx, gradedSPRBlock graderStake.Gra return nil } +// ApplyGradedDelegatedSPRBlock pays out PEG to the winners of the given GradedBlock. +// If an error is returned, the sql.Tx should be rolled back by the caller. +func (d *Pegnetd) ApplyGradedDelegatedSPRBlock(tx *sql.Tx, gradedSPRBlock graderDelegateStake.DelegatedGradedBlock, timestamp time.Time) error { + winners := gradedSPRBlock.Winners() + for i := range winners { + addr, err := factom.NewFAAddress(winners[i].SPR.GetAddress()) + if err != nil { + // TODO: This is kinda an odd case. I think we should just drop the rewards + // for an invalid address. We can always add back the rewards and they will have + // a higher balance after a change. + log.WithError(err).WithFields(log.Fields{ + "height": winners[i].SPR.GetHeight(), + "ehash": fmt.Sprintf("%x", winners[i].EntryHash), + }).Warnf("failed to reward") + continue + } + + if _, err := d.Pegnet.AddToBalance(tx, &addr, fat2.PTickerPEG, uint64(winners[i].Payout())); err != nil { + return err + } + + if err := d.Pegnet.InsertStaking100CoinbaseDelegate(tx, winners[i], addr[:], timestamp); err != nil { + return err + } + } + return nil +} + func isDone(ctx context.Context) bool { select { case <-ctx.Done():