Skip to content

Commit

Permalink
Rosetta Implementation Cleanup FIX (Stage 3 of Node API Overhaul) (#3402
Browse files Browse the repository at this point in the history
)

* [rosetta] Fix pre-staking rewards with GetPreStakingBlockRewards

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [hmy] Refactor GetDetailedBlockSignerInfo to use availability

* Add GetPreStakingBlockRewards with cache

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Fix prestaking block reward unit tests

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Report tx fees of coinbase for prestaking rewards

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>
  • Loading branch information
Daniel-VDM authored Oct 23, 2020
1 parent 1a915c0 commit 046e87e
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 184 deletions.
112 changes: 88 additions & 24 deletions hmy/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/ethereum/go-ethereum/rpc"
"github.com/harmony-one/harmony/block"
"github.com/harmony-one/harmony/core"
"github.com/harmony-one/harmony/core/rawdb"
"github.com/harmony-one/harmony/core/state"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/crypto/bls"
Expand All @@ -19,6 +20,8 @@ import (
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/internal/utils"
"github.com/harmony-one/harmony/shard"
"github.com/harmony-one/harmony/staking/availability"
stakingNetwork "github.com/harmony-one/harmony/staking/network"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -75,47 +78,108 @@ func (hmy *Harmony) GetBlockSigners(

// DetailedBlockSignerInfo contains all of the block singing information
type DetailedBlockSignerInfo struct {
// Signers is a map of addresses in the Signers for the block to
// all of the serialized BLS keys that signed said block.
Signers map[common.Address][]bls.SerializedPublicKey
// Signers are all the signers for the block
Signers shard.SlotList
// Committee when the block was signed.
Committee shard.SlotList
// TotalKeysSigned is the total number of bls keys that signed the block.
TotalKeysSigned uint
// Mask is the bitmap Mask for the block.
Mask *bls.Mask
BlockHash common.Hash
}

// GetDetailedBlockSignerInfo fetches the block signer information for any non-genesis block
func (hmy *Harmony) GetDetailedBlockSignerInfo(
ctx context.Context, blk *types.Block,
) (*DetailedBlockSignerInfo, error) {
slotList, mask, err := hmy.GetBlockSigners(
ctx, rpc.BlockNumber(blk.Number().Uint64()),
parentBlk, err := hmy.BlockByNumber(ctx, rpc.BlockNumber(blk.NumberU64()-1))
if err != nil {
return nil, err
}
parentShardState, err := hmy.BlockChain.ReadShardState(parentBlk.Epoch())
if err != nil {
return nil, err
}
committee, signers, _, err := availability.BallotResult(
parentBlk.Header(), blk.Header(), parentShardState, blk.ShardID(),
)
return &DetailedBlockSignerInfo{
Signers: signers,
Committee: committee,
BlockHash: blk.Hash(),
}, nil
}

// PreStakingBlockRewards are the rewards for a block in the pre-staking era (epoch < staking epoch).
type PreStakingBlockRewards map[common.Address]*big.Int

// GetPreStakingBlockRewards for the given block number.
// Calculated rewards are done exactly like chain.AccumulateRewardsAndCountSigs.
func (hmy *Harmony) GetPreStakingBlockRewards(
ctx context.Context, blk *types.Block,
) (PreStakingBlockRewards, error) {
if hmy.IsStakingEpoch(blk.Epoch()) {
return nil, fmt.Errorf("block %v is in staking era", blk.Number())
}

if cachedReward, ok := hmy.preStakingBlockRewardsCache.Get(blk.Hash()); ok {
return cachedReward.(PreStakingBlockRewards), nil
}
rewards := PreStakingBlockRewards{}

sigInfo, err := hmy.GetDetailedBlockSignerInfo(ctx, blk)
if err != nil {
return nil, err
}
last := big.NewInt(0)
count := big.NewInt(int64(len(sigInfo.Signers)))
for i, slot := range sigInfo.Signers {
rewardsForThisAddr, ok := rewards[slot.EcdsaAddress]
if !ok {
rewardsForThisAddr = big.NewInt(0)
}
cur := big.NewInt(0)
cur.Mul(stakingNetwork.BlockReward, big.NewInt(int64(i+1))).Div(cur, count)
reward := big.NewInt(0).Sub(cur, last)
rewards[slot.EcdsaAddress] = new(big.Int).Add(reward, rewardsForThisAddr)
last = cur
}

totalSigners := uint(0)
sigInfos := map[common.Address][]bls.SerializedPublicKey{}
for _, slot := range slotList {
if _, ok := sigInfos[slot.EcdsaAddress]; !ok {
sigInfos[slot.EcdsaAddress] = []bls.SerializedPublicKey{}
// Report tx fees of the coinbase (== leader)
receipts, err := hmy.GetReceipts(ctx, blk.Hash())
if err != nil {
return nil, err
}
txFees := big.NewInt(0)
for _, tx := range blk.Transactions() {
dbTx, _, _, receiptIndex := rawdb.ReadTransaction(hmy.ChainDb(), tx.Hash())
if dbTx == nil {
return nil, fmt.Errorf("could not find receipt for tx: %v", tx.Hash().String())
}
if ok, err := mask.KeyEnabled(slot.BLSPublicKey); ok && err == nil {
sigInfos[slot.EcdsaAddress] = append(sigInfos[slot.EcdsaAddress], slot.BLSPublicKey)
totalSigners++
if len(receipts) <= int(receiptIndex) {
return nil, fmt.Errorf("invalid receipt indext %v (>= num receipts: %v) for tx: %v",
receiptIndex, len(receipts), tx.Hash().String())
}
txFee := new(big.Int).Mul(tx.GasPrice(), big.NewInt(int64(receipts[receiptIndex].GasUsed)))
txFees = new(big.Int).Add(txFee, txFees)
}
return &DetailedBlockSignerInfo{
Signers: sigInfos,
Committee: slotList,
TotalKeysSigned: totalSigners,
Mask: mask,
BlockHash: blk.Hash(),
}, nil
for _, stx := range blk.StakingTransactions() {
dbsTx, _, _, receiptIndex := rawdb.ReadStakingTransaction(hmy.ChainDb(), stx.Hash())
if dbsTx == nil {
return nil, fmt.Errorf("could not find receipt for tx: %v", stx.Hash().String())
}
if len(receipts) <= int(receiptIndex) {
return nil, fmt.Errorf("invalid receipt indext %v (>= num receipts: %v) for tx: %v",
receiptIndex, len(receipts), stx.Hash().String())
}
txFee := new(big.Int).Mul(stx.GasPrice(), big.NewInt(int64(receipts[receiptIndex].GasUsed)))
txFees = new(big.Int).Add(txFee, txFees)
}
if amt, ok := rewards[blk.Header().Coinbase()]; ok {
rewards[blk.Header().Coinbase()] = new(big.Int).Add(amt, txFees)
} else {
rewards[blk.Header().Coinbase()] = txFees
}

hmy.preStakingBlockRewardsCache.Add(blk.Hash(), rewards)
return rewards, nil
}

// GetLatestChainHeaders ..
Expand Down
43 changes: 24 additions & 19 deletions hmy/hmy.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ import (
const (
// BloomBitsBlocks is the number of blocks a single bloom bit section vector
// contains on the server side.
BloomBitsBlocks uint64 = 4096
leaderCacheSize = 250 // Approx number of BLS keys in committee
undelegationPayoutsCacheSize = 500 // max number of epochs to store in cache
totalStakeCacheDuration = 20 // number of blocks where the returned total stake will remain the same
BloomBitsBlocks uint64 = 4096
leaderCacheSize = 250 // Approx number of BLS keys in committee
undelegationPayoutsCacheSize = 500 // max number of epochs to store in cache
preStakingBlockRewardsCacheSize = 1024 // max number of block rewards to store in cache
totalStakeCacheDuration = 20 // number of blocks where the returned total stake will remain the same
)

var (
Expand Down Expand Up @@ -67,6 +68,8 @@ type Harmony struct {
leaderCache *lru.Cache
// undelegationPayoutsCache to save on recomputation every epoch
undelegationPayoutsCache *lru.Cache
// preStakingBlockRewardsCache to save on recomputation for commonly checked blocks in epoch < staking epoch
preStakingBlockRewardsCache *lru.Cache
// totalStakeCache to save on recomputation for `totalStakeCacheDuration` blocks.
totalStakeCache *totalStakeCache
}
Expand Down Expand Up @@ -111,25 +114,27 @@ func New(
chainDb := nodeAPI.Blockchain().ChainDB()
leaderCache, _ := lru.New(leaderCacheSize)
undelegationPayoutsCache, _ := lru.New(undelegationPayoutsCacheSize)
preStakingBlockRewardsCache, _ := lru.New(preStakingBlockRewardsCacheSize)
totalStakeCache := newTotalStakeCache(totalStakeCacheDuration)
bloomIndexer := NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms)
bloomIndexer.Start(nodeAPI.Blockchain())
return &Harmony{
ShutdownChan: make(chan bool),
BloomRequests: make(chan chan *bloombits.Retrieval),
BloomIndexer: bloomIndexer,
BlockChain: nodeAPI.Blockchain(),
BeaconChain: nodeAPI.Beaconchain(),
TxPool: txPool,
CxPool: cxPool,
eventMux: new(event.TypeMux),
chainDb: chainDb,
NodeAPI: nodeAPI,
ChainID: nodeAPI.Blockchain().Config().ChainID.Uint64(),
ShardID: shardID,
leaderCache: leaderCache,
totalStakeCache: totalStakeCache,
undelegationPayoutsCache: undelegationPayoutsCache,
ShutdownChan: make(chan bool),
BloomRequests: make(chan chan *bloombits.Retrieval),
BloomIndexer: bloomIndexer,
BlockChain: nodeAPI.Blockchain(),
BeaconChain: nodeAPI.Beaconchain(),
TxPool: txPool,
CxPool: cxPool,
eventMux: new(event.TypeMux),
chainDb: chainDb,
NodeAPI: nodeAPI,
ChainID: nodeAPI.Blockchain().Config().ChainID.Uint64(),
ShardID: shardID,
leaderCache: leaderCache,
totalStakeCache: totalStakeCache,
undelegationPayoutsCache: undelegationPayoutsCache,
preStakingBlockRewardsCache: preStakingBlockRewardsCache,
}
}

Expand Down
29 changes: 7 additions & 22 deletions rosetta/services/block_special.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,31 +155,23 @@ func (s *BlockAPI) specialGenesisBlockTransaction(
}

// getPreStakingRewardTransactionIdentifiers is only used for the /block endpoint
// rewards for signing block n is paid out on block n+1
func (s *BlockAPI) getPreStakingRewardTransactionIdentifiers(
ctx context.Context, currBlock *hmytypes.Block,
) ([]*types.TransactionIdentifier, *types.Error) {
if currBlock.Number().Cmp(big.NewInt(1)) != 1 {
return nil, nil
}
blockNumToBeRewarded := currBlock.Number().Uint64() - 1
rewardedBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blockNumToBeRewarded).EthBlockNumber())
if err != nil {
return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{
"message": err.Error(),
})
}
blockSigInfo, err := s.hmy.GetDetailedBlockSignerInfo(ctx, rewardedBlock)
rewards, err := s.hmy.GetPreStakingBlockRewards(ctx, currBlock)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
txIDs := []*types.TransactionIdentifier{}
for acc, signedBlsKeys := range blockSigInfo.Signers {
if len(signedBlsKeys) > 0 {
txIDs = append(txIDs, getSpecialCaseTransactionIdentifier(currBlock.Hash(), acc, SpecialPreStakingRewardTxID))
}
for addr := range rewards {
txIDs = append(txIDs, getSpecialCaseTransactionIdentifier(
currBlock.Hash(), addr, SpecialPreStakingRewardTxID,
))
}
return txIDs, nil
}
Expand Down Expand Up @@ -221,27 +213,20 @@ func (s *BlockAPI) preStakingRewardBlockTransaction(
if rosettaError != nil {
return nil, rosettaError
}
blockNumOfSigsForReward := blk.Number().Uint64() - 1
signedBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blockNumOfSigsForReward).EthBlockNumber())
if err != nil {
return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{
"message": err.Error(),
})
}
if blkHash.String() != blk.Hash().String() {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": fmt.Sprintf(
"block hash %v != requested block hash %v in tx ID", blkHash.String(), blk.Hash().String(),
),
})
}
blockSignerInfo, err := s.hmy.GetDetailedBlockSignerInfo(ctx, signedBlock)
rewards, err := s.hmy.GetPreStakingBlockRewards(ctx, blk)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
transactions, rosettaError := FormatPreStakingRewardTransaction(txID, blockSignerInfo, address)
transactions, rosettaError := FormatPreStakingRewardTransaction(txID, rewards, address)
if rosettaError != nil {
return nil, rosettaError
}
Expand Down
43 changes: 8 additions & 35 deletions rosetta/services/tx_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/harmony-one/harmony/hmy"
internalCommon "github.com/harmony-one/harmony/internal/common"
"github.com/harmony-one/harmony/rosetta/common"
stakingNetwork "github.com/harmony-one/harmony/staking/network"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)

Expand Down Expand Up @@ -176,44 +175,18 @@ func FormatGenesisTransaction(

// FormatPreStakingRewardTransaction for block rewards in pre-staking era for a given Bech-32 address.
func FormatPreStakingRewardTransaction(
txID *types.TransactionIdentifier, blockSigInfo *hmy.DetailedBlockSignerInfo, address ethcommon.Address,
txID *types.TransactionIdentifier, rewards hmy.PreStakingBlockRewards, address ethcommon.Address,
) (*types.Transaction, *types.Error) {
signatures, ok := blockSigInfo.Signers[address]
if !ok || len(signatures) == 0 {
return nil, &common.TransactionNotFoundError
}
accID, rosettaError := newAccountIdentifier(address)
if rosettaError != nil {
return nil, rosettaError
}

// Calculate rewards exactly like `AccumulateRewardsAndCountSigs` but short circuit when possible.
// WARNING: must do calculation in the order of the committee to get accurate values.
i := 0
last := big.NewInt(0)
rewardsForThisBlock := big.NewInt(0)
count := big.NewInt(int64(blockSigInfo.TotalKeysSigned))
for _, slot := range blockSigInfo.Committee {
rewardsForThisAddr := big.NewInt(0)
if keys, ok := blockSigInfo.Signers[slot.EcdsaAddress]; ok {
for range keys {
cur := big.NewInt(0)
cur.Mul(stakingNetwork.BlockReward, big.NewInt(int64(i+1))).Div(cur, count)
reward := big.NewInt(0).Sub(cur, last)
rewardsForThisAddr = new(big.Int).Add(reward, rewardsForThisAddr)
last = cur
i++
}
}
if slot.EcdsaAddress == address {
rewardsForThisBlock = rewardsForThisAddr
if !(rewardsForThisAddr.Cmp(big.NewInt(0)) > 0) {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": "expected non-zero block reward in pre-staking era for block signer",
})
}
break
}
value, ok := rewards[address]
if !ok {
return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{
"message": fmt.Sprintf("%v does not have any rewards for block",
internalCommon.MustAddressToBech32(address)),
})
}

return &types.Transaction{
Expand All @@ -227,7 +200,7 @@ func FormatPreStakingRewardTransaction(
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: rewardsForThisBlock.String(),
Value: value.String(),
Currency: &common.NativeCurrency,
},
},
Expand Down
Loading

0 comments on commit 046e87e

Please sign in to comment.