Rosetta Implementation - pt2 FIX2 (Stage 3.2 of Node API Overhaul) (#3338)

* [rosetta] Update staking operations to account for re-delegation

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

* [hmy] Add GetUndelegationChange

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

* [hmy] Add GetAllUndelegatedDelegators

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

* [hmy] Fix GetAllUndelegatedDelegators & add GetDelegationLockingPeriodInEpoch

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

* [rosetta] Fix block reward TX ID formatting

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

* [hmy] Remove unused GetUndelegationChange

* Fix GetUndelegationPayouts
* Add GetDelegationsByValidatorAtBlock
* Keep beaconchain usage at a minimum

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

* [hmy] Remove debug print & update comments for GetUndelegationPayouts

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

* [core] Add last garbage collected number to blockchain.go

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

* [rosetta] Add oldest block ID in net stat for non-archival nodes

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

* [rosetta] Fix network oldest block case when garb col blk unknown

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

* [core] Rename lastGarbCollectedBlkNum to maxGarbCollectedBlkNum

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

* [internal/chain] Refactor token lock period getter & expose

* Use refactored token lock period getter

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

* [hmy] Add UndelegationPayouts type

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

* [rosetta] Improve NewError detail failure message

* Add UndelegationPayoutOperation
* Rename PreStakingEraBlockRewardOperation to UndelegationPayoutOperation

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

* [rosetta] Integrate Correct undelegation payout operations

* Refactor special case transaction handeling & add helper functions
for determanining when payouts should be calculated
* Make getBlockSignerInfo a method of BlockAPI
* Rename constants for clarity
* Add unit tests for formatting Undelegation payout special transaction

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

* [hmy] Add caching to GetUndelegationPayouts

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

* [hmy] Nit - fix comment

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

* [rosetta] Add block not found error

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

* [rosetta] Refactor special case txID to be for general

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

* [rosetta] Fix lint

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

* [rosetta] Nit - fix comment

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

* [hmy] Nit - Make GetUndelegationPayouts more readable

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>
pull/3343/head
Daniel Van Der Maden 4 years ago committed by GitHub
parent f42338c30b
commit e74ab0bc0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      core/blockchain.go
  2. 39
      hmy/hmy.go
  3. 84
      hmy/staking.go
  4. 18
      internal/chain/engine.go
  5. 13
      rosetta/common/errors.go
  6. 10
      rosetta/common/operations.go
  7. 3
      rosetta/common/operations_test.go
  8. 404
      rosetta/services/block.go
  9. 97
      rosetta/services/block_test.go
  10. 33
      rosetta/services/network.go

@ -161,13 +161,14 @@ type BlockChain struct {
procInterrupt int32 // interrupt signaler for block processing procInterrupt int32 // interrupt signaler for block processing
wg sync.WaitGroup // chain processing wait group for shutting down wg sync.WaitGroup // chain processing wait group for shutting down
engine consensus_engine.Engine engine consensus_engine.Engine
processor Processor // block processor interface processor Processor // block processor interface
validator Validator // block and state validator interface validator Validator // block and state validator interface
vmConfig vm.Config vmConfig vm.Config
badBlocks *lru.Cache // Bad block cache badBlocks *lru.Cache // Bad block cache
shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block.
pendingSlashes slash.Records pendingSlashes slash.Records
maxGarbCollectedBlkNum int64
} }
// NewBlockChain returns a fully initialised block chain using information // NewBlockChain returns a fully initialised block chain using information
@ -228,6 +229,7 @@ func NewBlockChain(
vmConfig: vmConfig, vmConfig: vmConfig,
badBlocks: badBlocks, badBlocks: badBlocks,
pendingSlashes: slash.Records{}, pendingSlashes: slash.Records{},
maxGarbCollectedBlkNum: -1,
} }
bc.SetValidator(NewBlockValidator(chainConfig, bc, engine)) bc.SetValidator(NewBlockValidator(chainConfig, bc, engine))
bc.SetProcessor(NewStateProcessor(chainConfig, bc, engine)) bc.SetProcessor(NewStateProcessor(chainConfig, bc, engine))
@ -1168,6 +1170,9 @@ func (bc *BlockChain) WriteBlockWithState(
bc.triegc.Push(root, number) bc.triegc.Push(root, number)
break break
} }
if -number > bc.maxGarbCollectedBlkNum {
bc.maxGarbCollectedBlkNum = -number
}
triedb.Dereference(root.(common.Hash)) triedb.Dereference(root.(common.Hash))
} }
} }
@ -1202,6 +1207,11 @@ func (bc *BlockChain) WriteBlockWithState(
return CanonStatTy, nil return CanonStatTy, nil
} }
// GetMaxGarbageCollectedBlockNumber ..
func (bc *BlockChain) GetMaxGarbageCollectedBlockNumber() int64 {
return bc.maxGarbCollectedBlkNum
}
// InsertChain attempts to insert the given batch of blocks in to the canonical // InsertChain attempts to insert the given batch of blocks in to the canonical
// chain or, otherwise, create a fork. If an error is returned it will return // chain or, otherwise, create a fork. If an error is returned it will return
// the index number of the failing block as well an error describing what went // the index number of the failing block as well an error describing what went
@ -2021,10 +2031,10 @@ func (bc *BlockChain) ReadPendingCrossLinks() ([]types.CrossLink, error) {
// WritePendingCrossLinks saves the pending crosslinks // WritePendingCrossLinks saves the pending crosslinks
func (bc *BlockChain) WritePendingCrossLinks(crossLinks []types.CrossLink) error { func (bc *BlockChain) WritePendingCrossLinks(crossLinks []types.CrossLink) error {
// deduplicate crosslinks if any // deduplicate crosslinks if any
m := map[uint32]map[uint64](types.CrossLink){} m := map[uint32]map[uint64]types.CrossLink{}
for _, cl := range crossLinks { for _, cl := range crossLinks {
if _, ok := m[cl.ShardID()]; !ok { if _, ok := m[cl.ShardID()]; !ok {
m[cl.ShardID()] = map[uint64](types.CrossLink){} m[cl.ShardID()] = map[uint64]types.CrossLink{}
} }
m[cl.ShardID()][cl.BlockNum()] = cl m[cl.ShardID()][cl.BlockNum()] = cl
} }
@ -2111,10 +2121,10 @@ func (bc *BlockChain) DeleteFromPendingCrossLinks(crossLinks []types.CrossLink)
return 0, err return 0, err
} }
m := map[uint32]map[uint64](struct{}){} m := map[uint32]map[uint64]struct{}{}
for _, cl := range crossLinks { for _, cl := range crossLinks {
if _, ok := m[cl.ShardID()]; !ok { if _, ok := m[cl.ShardID()]; !ok {
m[cl.ShardID()] = map[uint64](struct{}){} m[cl.ShardID()] = map[uint64]struct{}{}
} }
m[cl.ShardID()][cl.BlockNum()] = struct{}{} m[cl.ShardID()][cl.BlockNum()] = struct{}{}
} }

@ -29,9 +29,10 @@ import (
const ( const (
// BloomBitsBlocks is the number of blocks a single bloom bit section vector // BloomBitsBlocks is the number of blocks a single bloom bit section vector
// contains on the server side. // contains on the server side.
BloomBitsBlocks uint64 = 4096 BloomBitsBlocks uint64 = 4096
leaderCacheSize = 250 // Approx number of BLS keys in committee leaderCacheSize = 250 // Approx number of BLS keys in committee
totalStakeCacheDuration = 20 // number of blocks where the returned total stake will remain the same undelegationPayoutsCacheSize = 500 // max number of epochs to store in cache
totalStakeCacheDuration = 20 // number of blocks where the returned total stake will remain the same
) )
var ( var (
@ -64,6 +65,8 @@ type Harmony struct {
group singleflight.Group group singleflight.Group
// leaderCache to save on recomputation every epoch. // leaderCache to save on recomputation every epoch.
leaderCache *lru.Cache leaderCache *lru.Cache
// undelegationPayoutsCache to save on recomputation every epoch
undelegationPayoutsCache *lru.Cache
// totalStakeCache to save on recomputation for `totalStakeCacheDuration` blocks. // totalStakeCache to save on recomputation for `totalStakeCacheDuration` blocks.
totalStakeCache *totalStakeCache totalStakeCache *totalStakeCache
} }
@ -98,24 +101,26 @@ func New(
) *Harmony { ) *Harmony {
chainDb := nodeAPI.Blockchain().ChainDB() chainDb := nodeAPI.Blockchain().ChainDB()
leaderCache, _ := lru.New(leaderCacheSize) leaderCache, _ := lru.New(leaderCacheSize)
undelegationPayoutsCache, _ := lru.New(undelegationPayoutsCacheSize)
totalStakeCache := newTotalStakeCache(totalStakeCacheDuration) totalStakeCache := newTotalStakeCache(totalStakeCacheDuration)
bloomIndexer := NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms) bloomIndexer := NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms)
bloomIndexer.Start(nodeAPI.Blockchain()) bloomIndexer.Start(nodeAPI.Blockchain())
return &Harmony{ return &Harmony{
ShutdownChan: make(chan bool), ShutdownChan: make(chan bool),
BloomRequests: make(chan chan *bloombits.Retrieval), BloomRequests: make(chan chan *bloombits.Retrieval),
BloomIndexer: bloomIndexer, BloomIndexer: bloomIndexer,
BlockChain: nodeAPI.Blockchain(), BlockChain: nodeAPI.Blockchain(),
BeaconChain: nodeAPI.Beaconchain(), BeaconChain: nodeAPI.Beaconchain(),
TxPool: txPool, TxPool: txPool,
CxPool: cxPool, CxPool: cxPool,
eventMux: new(event.TypeMux), eventMux: new(event.TypeMux),
chainDb: chainDb, chainDb: chainDb,
NodeAPI: nodeAPI, NodeAPI: nodeAPI,
ChainID: nodeAPI.Blockchain().Config().ChainID.Uint64(), ChainID: nodeAPI.Blockchain().Config().ChainID.Uint64(),
ShardID: shardID, ShardID: shardID,
leaderCache: leaderCache, leaderCache: leaderCache,
totalStakeCache: totalStakeCache, totalStakeCache: totalStakeCache,
undelegationPayoutsCache: undelegationPayoutsCache,
} }
} }

@ -7,10 +7,12 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/consensus/quorum"
"github.com/harmony-one/harmony/core/rawdb" "github.com/harmony-one/harmony/core/rawdb"
"github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/types"
internal_common "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/chain"
internalCommon "github.com/harmony-one/harmony/internal/common"
"github.com/harmony-one/harmony/numeric" "github.com/harmony-one/harmony/numeric"
commonRPC "github.com/harmony-one/harmony/rpc/common" commonRPC "github.com/harmony-one/harmony/rpc/common"
"github.com/harmony-one/harmony/shard" "github.com/harmony-one/harmony/shard"
@ -22,7 +24,8 @@ import (
) )
var ( var (
zero = numeric.ZeroDec() zero = numeric.ZeroDec()
bigZero = big.NewInt(0)
) )
func (hmy *Harmony) readAndUpdateRawStakes( func (hmy *Harmony) readAndUpdateRawStakes(
@ -125,6 +128,16 @@ func (hmy *Harmony) IsStakingEpoch(epoch *big.Int) bool {
return hmy.BlockChain.Config().IsStaking(epoch) return hmy.BlockChain.Config().IsStaking(epoch)
} }
// IsPreStakingEpoch ...
func (hmy *Harmony) IsPreStakingEpoch(epoch *big.Int) bool {
return hmy.BlockChain.Config().IsPreStaking(epoch)
}
// GetDelegationLockingPeriodInEpoch ...
func (hmy *Harmony) GetDelegationLockingPeriodInEpoch(epoch *big.Int) int {
return chain.GetLockPeriodInEpoch(hmy.BlockChain, epoch)
}
// SendStakingTx adds a staking transaction // SendStakingTx adds a staking transaction
func (hmy *Harmony) SendStakingTx(ctx context.Context, signedStakingTx *staking.StakingTransaction) error { func (hmy *Harmony) SendStakingTx(ctx context.Context, signedStakingTx *staking.StakingTransaction) error {
stx, _, _, _ := rawdb.ReadStakingTransaction(hmy.chainDb, signedStakingTx.Hash()) stx, _, _, _ := rawdb.ReadStakingTransaction(hmy.chainDb, signedStakingTx.Hash())
@ -252,7 +265,7 @@ func (hmy *Harmony) GetValidatorInformation(
bc := hmy.BlockChain bc := hmy.BlockChain
wrapper, err := bc.ReadValidatorInformationAt(addr, block.Root()) wrapper, err := bc.ReadValidatorInformationAt(addr, block.Root())
if err != nil { if err != nil {
s, _ := internal_common.AddressToBech32(addr) s, _ := internalCommon.AddressToBech32(addr)
return nil, errors.Wrapf(err, "not found address in current state %s", s) return nil, errors.Wrapf(err, "not found address in current state %s", s)
} }
@ -433,6 +446,21 @@ func (hmy *Harmony) GetDelegationsByValidator(validator common.Address) []*staki
return delegations return delegations
} }
// GetDelegationsByValidatorAtBlock returns all delegation information of a validator at the given block
func (hmy *Harmony) GetDelegationsByValidatorAtBlock(
validator common.Address, block *types.Block,
) []*staking.Delegation {
wrapper, err := hmy.BlockChain.ReadValidatorInformationAt(validator, block.Root())
if err != nil || wrapper == nil {
return nil
}
delegations := []*staking.Delegation{}
for i := range wrapper.Delegations {
delegations = append(delegations, &wrapper.Delegations[i])
}
return delegations
}
// GetDelegationsByDelegator returns all delegation information of a delegator // GetDelegationsByDelegator returns all delegation information of a delegator
func (hmy *Harmony) GetDelegationsByDelegator( func (hmy *Harmony) GetDelegationsByDelegator(
delegator common.Address, delegator common.Address,
@ -471,6 +499,56 @@ func (hmy *Harmony) GetDelegationsByDelegatorByBlock(
return addresses, delegations return addresses, delegations
} }
// UndelegationPayouts ..
type UndelegationPayouts map[common.Address]*big.Int
// GetUndelegationPayouts returns the undelegation payouts for each delegator
//
// Due to in-memory caching, it is possible to get undelegation payouts for a state / epoch
// that has been pruned but have it be lost (and unable to recompute) after the node restarts.
// This not a problem if a full (archival) DB is used.
func (hmy *Harmony) GetUndelegationPayouts(
ctx context.Context, epoch *big.Int,
) (UndelegationPayouts, error) {
if !hmy.IsPreStakingEpoch(epoch) {
return nil, fmt.Errorf("not pre-staking epoch or later")
}
payouts, ok := hmy.undelegationPayoutsCache.Get(epoch.Uint64())
if ok {
return payouts.(UndelegationPayouts), nil
}
undelegationPayouts := UndelegationPayouts{}
// require second to last block as saved undelegations are AFTER undelegations are payed out
blockNumber := shard.Schedule.EpochLastBlock(epoch.Uint64()) - 1
undelegationPayoutBlock, err := hmy.BlockByNumber(ctx, rpc.BlockNumber(blockNumber))
if err != nil || undelegationPayoutBlock == nil {
// Block not found, so no undelegationPayouts (not an error)
return undelegationPayouts, nil
}
lockingPeriod := hmy.GetDelegationLockingPeriodInEpoch(undelegationPayoutBlock.Epoch())
for _, validator := range hmy.GetAllValidatorAddresses() {
wrapper, err := hmy.BlockChain.ReadValidatorInformationAt(validator, undelegationPayoutBlock.Root())
if err != nil || wrapper == nil {
continue // Not a validator at this epoch or unable to fetch validator info because of pruned state.
}
for _, delegation := range wrapper.Delegations {
withdraw := delegation.RemoveUnlockedUndelegations(epoch, wrapper.LastEpochInCommittee, lockingPeriod)
if withdraw.Cmp(bigZero) == 1 {
if totalPayout, ok := undelegationPayouts[delegation.DelegatorAddress]; ok {
undelegationPayouts[delegation.DelegatorAddress] = new(big.Int).Add(totalPayout, withdraw)
} else {
undelegationPayouts[delegation.DelegatorAddress] = withdraw
}
}
}
}
hmy.undelegationPayoutsCache.Add(epoch.Uint64(), undelegationPayouts)
return undelegationPayouts, nil
}
// GetTotalStakingSnapshot .. // GetTotalStakingSnapshot ..
func (hmy *Harmony) GetTotalStakingSnapshot() *big.Int { func (hmy *Harmony) GetTotalStakingSnapshot() *big.Int {
if stake := hmy.totalStakeCache.pop(hmy.CurrentBlock().NumberU64()); stake != nil { if stake := hmy.totalStakeCache.pop(hmy.CurrentBlock().NumberU64()); stake != nil {

@ -292,12 +292,7 @@ func payoutUndelegations(
"[Finalize] failed to get validator from state to finalize", "[Finalize] failed to get validator from state to finalize",
) )
} }
lockPeriod := staking.LockPeriodInEpoch lockPeriod := GetLockPeriodInEpoch(chain, header.Epoch())
if chain.Config().IsRedelegation(header.Epoch()) {
lockPeriod = staking.LockPeriodInEpoch
} else if chain.Config().IsQuickUnlock(header.Epoch()) {
lockPeriod = staking.LockPeriodInEpochV2
}
for i := range wrapper.Delegations { for i := range wrapper.Delegations {
delegation := &wrapper.Delegations[i] delegation := &wrapper.Delegations[i]
totalWithdraw := delegation.RemoveUnlockedUndelegations( totalWithdraw := delegation.RemoveUnlockedUndelegations(
@ -547,3 +542,14 @@ func GetPublicKeys(
} }
return subCommittee.BLSPublicKeys() return subCommittee.BLSPublicKeys()
} }
// GetLockPeriodInEpoch returns the delegation lock period for the given chain
func GetLockPeriodInEpoch(chain engine.ChainReader, epoch *big.Int) int {
lockPeriod := staking.LockPeriodInEpoch
if chain.Config().IsRedelegation(epoch) {
lockPeriod = staking.LockPeriodInEpoch
} else if chain.Config().IsQuickUnlock(epoch) {
lockPeriod = staking.LockPeriodInEpochV2
}
return lockPeriod
}

@ -1,6 +1,8 @@
package common package common
import ( import (
"fmt"
"github.com/coinbase/rosetta-sdk-go/types" "github.com/coinbase/rosetta-sdk-go/types"
"github.com/harmony-one/harmony/rpc" "github.com/harmony-one/harmony/rpc"
) )
@ -65,15 +67,14 @@ var (
// NewError create a new error with a given detail structure // NewError create a new error with a given detail structure
func NewError(rosettaError types.Error, detailStructure interface{}) *types.Error { func NewError(rosettaError types.Error, detailStructure interface{}) *types.Error {
newError := rosettaError
details, err := rpc.NewStructuredResponse(detailStructure) details, err := rpc.NewStructuredResponse(detailStructure)
if err != nil { if err != nil {
newError := CatchAllError newError.Details = map[string]interface{}{
CatchAllError.Details = map[string]interface{}{ "message": fmt.Sprintf("unable to get error details: %v", err.Error()),
"message": err.Error(),
} }
return &newError } else {
newError.Details = details
} }
newError := rosettaError
newError.Details = details
return &newError return &newError
} }

@ -21,8 +21,11 @@ const (
// GenesisFundsOperation .. // GenesisFundsOperation ..
GenesisFundsOperation = "Genesis" GenesisFundsOperation = "Genesis"
// PreStakingEraBlockRewardOperation .. // PreStakingBlockRewardOperation ..
PreStakingEraBlockRewardOperation = "PreStakingBlockReward" PreStakingBlockRewardOperation = "PreOpenStakingBlockReward"
// UndelegationPayoutOperation ..
UndelegationPayoutOperation = "UndelegationPayout"
) )
var ( var (
@ -33,7 +36,8 @@ var (
CrossShardTransferOperation, CrossShardTransferOperation,
ContractCreationOperation, ContractCreationOperation,
GenesisFundsOperation, GenesisFundsOperation,
PreStakingEraBlockRewardOperation, PreStakingBlockRewardOperation,
UndelegationPayoutOperation,
} }
// StakingOperationTypes .. // StakingOperationTypes ..

@ -54,7 +54,8 @@ func TestPlainOperationTypes(t *testing.T) {
CrossShardTransferOperation, CrossShardTransferOperation,
ContractCreationOperation, ContractCreationOperation,
GenesisFundsOperation, GenesisFundsOperation,
PreStakingEraBlockRewardOperation, PreStakingBlockRewardOperation,
UndelegationPayoutOperation,
} }
sort.Strings(referenceOperationTypes) sort.Strings(referenceOperationTypes)
sort.Strings(plainOperationTypes) sort.Strings(plainOperationTypes)

@ -29,10 +29,6 @@ import (
stakingTypes "github.com/harmony-one/harmony/staking/types" stakingTypes "github.com/harmony-one/harmony/staking/types"
) )
const (
blockHashLen = 64
)
// BlockAPI implements the server.BlockAPIServicer interface. // BlockAPI implements the server.BlockAPIServicer interface.
type BlockAPI struct { type BlockAPI struct {
hmy *hmy.Harmony hmy *hmy.Harmony
@ -79,11 +75,18 @@ func (s *BlockAPI) Block(
Hash: prevBlock.Hash().String(), Hash: prevBlock.Hash().String(),
} }
// Report undelegation payouts as transactions to fit API.
// Report all transactions here since all undelegation payout amounts are known after fetching payouts.
transactions, rosettaError := s.getAllUndelegationPayoutTransactions(ctx, blk)
if rosettaError != nil {
return nil, rosettaError
}
responseBlock := &types.Block{ responseBlock := &types.Block{
BlockIdentifier: currBlockID, BlockIdentifier: currBlockID,
ParentBlockIdentifier: prevBlockID, ParentBlockIdentifier: prevBlockID,
Timestamp: blk.Time().Int64() * 1e3, // Timestamp must be in ms. Timestamp: blk.Time().Int64() * 1e3, // Timestamp must be in ms.
Transactions: []*types.Transaction{}, // Do not return tx details as it is optional. Transactions: transactions,
} }
otherTransactions := []*types.TransactionIdentifier{} otherTransactions := []*types.TransactionIdentifier{}
@ -99,6 +102,7 @@ func (s *BlockAPI) Block(
} }
// Report cross-shard transaction payouts. // Report cross-shard transaction payouts.
for _, cxReceipts := range blk.IncomingReceipts() { for _, cxReceipts := range blk.IncomingReceipts() {
// Report cross-shard transaction payouts.
for _, cxReceipt := range cxReceipts.Receipts { for _, cxReceipt := range cxReceipts.Receipts {
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{ otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
Hash: cxReceipt.TxHash.String(), Hash: cxReceipt.TxHash.String(),
@ -107,21 +111,11 @@ func (s *BlockAPI) Block(
} }
// Report pre-staking era block rewards as transactions to fit API. // Report pre-staking era block rewards as transactions to fit API.
if !s.hmy.IsStakingEpoch(blk.Epoch()) { if !s.hmy.IsStakingEpoch(blk.Epoch()) {
blockSigInfo, rosettaError := getBlockSignerInfo(ctx, s.hmy, blk) preStakingRewardTxIDs, rosettaError := s.getAllPreStakingRewardTransactionIdentifiers(ctx, blk)
if rosettaError != nil { if rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
for acc, signedBlsKeys := range blockSigInfo.signers { otherTransactions = append(otherTransactions, preStakingRewardTxIDs...)
if len(signedBlsKeys) > 0 {
b32Addr, err := internalCommon.AddressToBech32(acc)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
otherTransactions = append(otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), b32Addr))
}
}
} }
return &types.BlockResponse{ return &types.BlockResponse{
@ -157,13 +151,9 @@ func (s *BlockAPI) genesisBlock(
otherTransactions := []*types.TransactionIdentifier{} otherTransactions := []*types.TransactionIdentifier{}
// Report initial genesis funds as transactions to fit API. // Report initial genesis funds as transactions to fit API.
for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(blk.ShardID())) { for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(blk.ShardID())) {
b32Addr, err := internalCommon.AddressToBech32(*tx.To()) otherTransactions = append(
if err != nil { otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), *tx.To(), SpecialGenesisTxID),
return nil, common.NewError(common.CatchAllError, map[string]interface{}{ )
"message": err.Error(),
})
}
otherTransactions = append(otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), b32Addr))
} }
return &types.BlockResponse{ return &types.BlockResponse{
@ -172,10 +162,111 @@ func (s *BlockAPI) genesisBlock(
}, nil }, nil
} }
// getAllPreStakingRewardTransactionIdentifiers is only used for the /block endpoint
func (s *BlockAPI) getAllPreStakingRewardTransactionIdentifiers(
ctx context.Context, blk *hmytypes.Block,
) ([]*types.TransactionIdentifier, *types.Error) {
txIDs := []*types.TransactionIdentifier{}
blockSigInfo, rosettaError := s.getBlockSignerInfo(ctx, blk)
if rosettaError != nil {
return nil, rosettaError
}
for acc, signedBlsKeys := range blockSigInfo.signers {
if len(signedBlsKeys) > 0 {
txIDs = append(txIDs, getSpecialCaseTransactionIdentifier(blk.Hash(), acc, SpecialPreStakingRewardTxID))
}
}
return txIDs, nil
}
// isCommitteeSelectionBlock ..
func (s *BlockAPI) isCommitteeSelectionBlock(blk *hmytypes.Block) bool {
isBeaconChain := blk.ShardID() == shard.BeaconChainShardID
isNewEpoch := len(blk.Header().ShardState()) > 0
inPreStakingEra := s.hmy.IsPreStakingEpoch(blk.Epoch())
return isBeaconChain && isNewEpoch && inPreStakingEra
}
// getAllUndelegationPayoutTransactions is only used for the /block endpoint
func (s *BlockAPI) getAllUndelegationPayoutTransactions(
ctx context.Context, blk *hmytypes.Block,
) ([]*types.Transaction, *types.Error) {
if !s.isCommitteeSelectionBlock(blk) {
return []*types.Transaction{}, nil
}
delegatorPayouts, err := s.hmy.GetUndelegationPayouts(ctx, blk.Epoch())
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
transactions := []*types.Transaction{}
for delegator, payout := range delegatorPayouts {
accID, rosettaError := newAccountIdentifier(delegator)
if rosettaError != nil {
return nil, rosettaError
}
transactions = append(transactions, &types.Transaction{
TransactionIdentifier: getSpecialCaseTransactionIdentifier(
blk.Hash(), delegator, SpecialUndelegationPayoutTxID,
),
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0, // There is no gas expenditure for undelegation payout
},
Type: common.UndelegationPayoutOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", payout),
Currency: &common.Currency,
},
},
},
})
}
return transactions, nil
}
// getBlockSignerInfo fetches the block signer information for any non-genesis block
func (s *BlockAPI) getBlockSignerInfo(
ctx context.Context, blk *hmytypes.Block,
) (*blockSignerInfo, *types.Error) {
slotList, mask, err := s.hmy.GetBlockSigners(
ctx, rpc.BlockNumber(blk.Number().Uint64()).EthBlockNumber(),
)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
totalSigners := uint(0)
sigInfos := map[ethcommon.Address][]bls.SerializedPublicKey{}
for _, slot := range slotList {
if _, ok := sigInfos[slot.EcdsaAddress]; !ok {
sigInfos[slot.EcdsaAddress] = []bls.SerializedPublicKey{}
}
if ok, err := mask.KeyEnabled(slot.BLSPublicKey); ok && err == nil {
sigInfos[slot.EcdsaAddress] = append(sigInfos[slot.EcdsaAddress], slot.BLSPublicKey)
totalSigners++
}
}
return &blockSignerInfo{
signers: sigInfos,
totalKeysSigned: totalSigners,
mask: mask,
blockHash: blk.Hash(),
}, nil
}
// BlockTransaction implements the /block/transaction endpoint // BlockTransaction implements the /block/transaction endpoint
func (s *BlockAPI) BlockTransaction( func (s *BlockAPI) BlockTransaction(
ctx context.Context, request *types.BlockTransactionRequest, ctx context.Context, request *types.BlockTransactionRequest,
) (response *types.BlockTransactionResponse, rosettaError *types.Error) { ) (*types.BlockTransactionResponse, *types.Error) {
if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil { if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil {
return nil, err return nil, err
} }
@ -189,17 +280,14 @@ func (s *BlockAPI) BlockTransaction(
txHash := ethcommon.HexToHash(request.TransactionIdentifier.Hash) txHash := ethcommon.HexToHash(request.TransactionIdentifier.Hash)
txInfo, rosettaError := s.getTransactionInfo(ctx, blockHash, txHash) txInfo, rosettaError := s.getTransactionInfo(ctx, blockHash, txHash)
if rosettaError != nil { if rosettaError != nil {
blk, rosettaError2 := s.getBlock(ctx, &types.PartialBlockIdentifier{Index: &request.BlockIdentifier.Index}) // If no transaction info is found, check for special case transactions.
if rosettaError2 != nil { response, rosettaError2 := s.specialBlockTransaction(ctx, request)
return nil, common.NewError(common.CatchAllError, map[string]interface{}{ if rosettaError2 != nil && rosettaError2.Code != common.TransactionNotFoundError.Code {
"error": rosettaError2, return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{
"base_error": rosettaError, "from_error": rosettaError2,
}) })
} }
if s.hmy.IsStakingEpoch(blk.Epoch()) { return response, rosettaError2
return nil, rosettaError
}
return s.preStakingEraBlockRewardTransaction(ctx, request.TransactionIdentifier, blk)
} }
var transaction *types.Transaction var transaction *types.Transaction
@ -229,25 +317,51 @@ func (s *BlockAPI) genesisBlockTransaction(
"message": err.Error(), "message": err.Error(),
}) })
} }
blkHash, b32Addr, rosettaError := unpackSpecialCaseTransactionIdentifier(request.TransactionIdentifier) blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(
request.TransactionIdentifier, SpecialGenesisTxID,
)
if rosettaError != nil { if rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
if blkHash.String() != genesisBlock.Hash().String() { if blkHash.String() != genesisBlock.Hash().String() {
return nil, &common.TransactionNotFoundError return nil, &common.TransactionNotFoundError
} }
txs, rosettaError := formatGenesisTransaction(request.TransactionIdentifier, b32Addr, s.hmy.ShardID) txs, rosettaError := formatGenesisTransaction(request.TransactionIdentifier, address, s.hmy.ShardID)
if rosettaError != nil { if rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
return &types.BlockTransactionResponse{Transaction: txs}, nil return &types.BlockTransactionResponse{Transaction: txs}, nil
} }
// preStakingEraBlockRewardTransaction is a special handler for pre-staking era block reward transactions // specialBlockTransaction is a formatter for special, non-genesis, transactions
func (s *BlockAPI) preStakingEraBlockRewardTransaction( func (s *BlockAPI) specialBlockTransaction(
ctx context.Context, request *types.BlockTransactionRequest,
) (*types.BlockTransactionResponse, *types.Error) {
// If no transaction info is found, check for special case transactions.
blk, rosettaError := s.getBlock(ctx, &types.PartialBlockIdentifier{Index: &request.BlockIdentifier.Index})
if rosettaError != nil {
return nil, rosettaError
}
if s.isCommitteeSelectionBlock(blk) {
// Note that undelegation payout MUST be checked before reporting error in pre-staking & staking era.
response, rosettaError := s.undelegationPayoutBlockTransaction(ctx, request.TransactionIdentifier, blk)
if rosettaError != nil && !s.hmy.IsStakingEpoch(blk.Epoch()) && s.hmy.IsPreStakingEpoch(blk.Epoch()) {
// Handle edge case special transaction for pre-staking era
return s.preStakingRewardBlockTransaction(ctx, request.TransactionIdentifier, blk)
}
return response, rosettaError
}
if !s.hmy.IsStakingEpoch(blk.Epoch()) {
return s.preStakingRewardBlockTransaction(ctx, request.TransactionIdentifier, blk)
}
return nil, &common.TransactionNotFoundError
}
// preStakingRewardBlockTransaction is a special handler for pre-staking era
func (s *BlockAPI) preStakingRewardBlockTransaction(
ctx context.Context, txID *types.TransactionIdentifier, blk *hmytypes.Block, ctx context.Context, txID *types.TransactionIdentifier, blk *hmytypes.Block,
) (*types.BlockTransactionResponse, *types.Error) { ) (*types.BlockTransactionResponse, *types.Error) {
blkHash, b32Address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID) blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialPreStakingRewardTxID)
if rosettaError != nil { if rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
@ -258,11 +372,41 @@ func (s *BlockAPI) preStakingEraBlockRewardTransaction(
), ),
}) })
} }
blockSignerInfo, rosettaError := getBlockSignerInfo(ctx, s.hmy, blk) blockSignerInfo, rosettaError := s.getBlockSignerInfo(ctx, blk)
if rosettaError != nil { if rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
transactions, rosettaError := formatPreStakingBlockRewardsTransaction(b32Address, blockSignerInfo) transactions, rosettaError := formatPreStakingRewardTransaction(txID, blockSignerInfo, address)
if rosettaError != nil {
return nil, rosettaError
}
return &types.BlockTransactionResponse{Transaction: transactions}, nil
}
// undelegationPayoutBlockTransaction is a special handler for undelegation payout transactions
func (s *BlockAPI) undelegationPayoutBlockTransaction(
ctx context.Context, txID *types.TransactionIdentifier, blk *hmytypes.Block,
) (*types.BlockTransactionResponse, *types.Error) {
blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialUndelegationPayoutTxID)
if rosettaError != nil {
return nil, rosettaError
}
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(),
),
})
}
delegatorPayouts, err := s.hmy.GetUndelegationPayouts(ctx, blk.Epoch())
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
transactions, rosettaError := formatUndelegationPayoutTransaction(txID, delegatorPayouts, address)
if rosettaError != nil { if rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
@ -287,6 +431,9 @@ func (s *BlockAPI) getBlock(
"message": err.Error(), "message": err.Error(),
}) })
} }
if blk == nil {
return nil, &common.BlockNotFoundError
}
return blk, nil return blk, nil
} }
@ -372,30 +519,56 @@ func getPseudoTransactionForGenesis(spec *core.Genesis) []*hmytypes.Transaction
return txs return txs
} }
// SpecialTransactionSuffix ..
type SpecialTransactionSuffix uint
// Special transaction suffixes that are specific to the rosetta package
const (
SpecialGenesisTxID SpecialTransactionSuffix = iota
SpecialPreStakingRewardTxID
SpecialUndelegationPayoutTxID
)
// String ..
func (s SpecialTransactionSuffix) String() string {
return [...]string{"genesis", "reward", "undelegation"}[s]
}
// getSpecialCaseTransactionIdentifier fetches 'transaction identifiers' for a given block-hash and suffix. // getSpecialCaseTransactionIdentifier fetches 'transaction identifiers' for a given block-hash and suffix.
// Special cases include genesis transactions & pre-staking era block rewards. // Special cases include genesis transactions, pre-staking era block rewards, and undelegation payouts.
// Must include block hash to guarantee uniqueness of tx identifiers. // Must include block hash to guarantee uniqueness of tx identifiers.
func getSpecialCaseTransactionIdentifier( func getSpecialCaseTransactionIdentifier(
blockHash ethcommon.Hash, suffix string, blockHash ethcommon.Hash, address ethcommon.Address, suffix SpecialTransactionSuffix,
) *types.TransactionIdentifier { ) *types.TransactionIdentifier {
return &types.TransactionIdentifier{ return &types.TransactionIdentifier{
Hash: fmt.Sprintf("%v_%v", blockHash.String(), suffix), Hash: fmt.Sprintf("%v_%v_%v",
blockHash.String(), internalCommon.MustAddressToBech32(address), suffix.String(),
),
} }
} }
const (
blockHashStrLen = 64
b32AddrStrLen = 42
)
// unpackSpecialCaseTransactionIdentifier returns the suffix & blockHash if the txID is formatted correctly. // unpackSpecialCaseTransactionIdentifier returns the suffix & blockHash if the txID is formatted correctly.
func unpackSpecialCaseTransactionIdentifier( func unpackSpecialCaseTransactionIdentifier(
txID *types.TransactionIdentifier, txID *types.TransactionIdentifier, expectedSuffix SpecialTransactionSuffix,
) (ethcommon.Hash, string, *types.Error) { ) (ethcommon.Hash, ethcommon.Address, *types.Error) {
hash := txID.Hash hash := txID.Hash
hash = strings.TrimPrefix(hash, "0x") hash = strings.TrimPrefix(hash, "0x")
hash = strings.TrimPrefix(hash, "0X") hash = strings.TrimPrefix(hash, "0X")
if len(hash) < blockHashLen+1 || string(hash[blockHashLen]) != "_" { minCharCount := blockHashStrLen + b32AddrStrLen + 2
return ethcommon.Hash{}, "", common.NewError(common.CatchAllError, map[string]interface{}{ if len(hash) < minCharCount || string(hash[blockHashStrLen]) != "_" ||
string(hash[minCharCount-1]) != "_" || expectedSuffix.String() != hash[minCharCount:] {
return ethcommon.Hash{}, ethcommon.Address{}, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unknown special case transaction ID format", "message": "unknown special case transaction ID format",
}) })
} }
return ethcommon.HexToHash(hash[:blockHashLen]), hash[blockHashLen+1:], nil blkHash := ethcommon.HexToHash(hash[:blockHashStrLen])
addr := internalCommon.MustBech32ToAddress(hash[blockHashStrLen+1 : minCharCount-1])
return blkHash, addr, nil
} }
// blockSignerInfo contains all of the block singing information // blockSignerInfo contains all of the block singing information
@ -410,38 +583,6 @@ type blockSignerInfo struct {
blockHash ethcommon.Hash blockHash ethcommon.Hash
} }
// getBlockSignerInfo fetches the block signer information for any non-genesis block
func getBlockSignerInfo(
ctx context.Context, hmy *hmy.Harmony, blk *hmytypes.Block,
) (*blockSignerInfo, *types.Error) {
slotList, mask, err := hmy.GetBlockSigners(
ctx, rpc.BlockNumber(blk.Number().Uint64()).EthBlockNumber(),
)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
totalSigners := uint(0)
sigInfos := map[ethcommon.Address][]bls.SerializedPublicKey{}
for _, slot := range slotList {
if _, ok := sigInfos[slot.EcdsaAddress]; !ok {
sigInfos[slot.EcdsaAddress] = []bls.SerializedPublicKey{}
}
if ok, err := mask.KeyEnabled(slot.BLSPublicKey); ok && err == nil {
sigInfos[slot.EcdsaAddress] = append(sigInfos[slot.EcdsaAddress], slot.BLSPublicKey)
totalSigners++
}
}
return &blockSignerInfo{
signers: sigInfos,
totalKeysSigned: totalSigners,
mask: mask,
blockHash: blk.Hash(),
}, nil
}
// TransactionMetadata .. // TransactionMetadata ..
type TransactionMetadata struct { type TransactionMetadata struct {
CrossShardIdentifier *types.TransactionIdentifier `json:"cross_shard_transaction_identifier,omitempty"` CrossShardIdentifier *types.TransactionIdentifier `json:"cross_shard_transaction_identifier,omitempty"`
@ -497,17 +638,13 @@ func formatCrossShardReceiverTransaction(
// formatGenesisTransaction for genesis block's initial balances // formatGenesisTransaction for genesis block's initial balances
func formatGenesisTransaction( func formatGenesisTransaction(
txID *types.TransactionIdentifier, targetB32Addr string, shardID uint32, txID *types.TransactionIdentifier, targetAddr ethcommon.Address, shardID uint32,
) (fmtTx *types.Transaction, rosettaError *types.Error) { ) (fmtTx *types.Transaction, rosettaError *types.Error) {
var b32Addr string var b32Addr string
var err error targetB32Addr := internalCommon.MustAddressToBech32(targetAddr)
genesisSpec := getGenesisSpec(shardID) genesisSpec := getGenesisSpec(shardID)
for _, tx := range getPseudoTransactionForGenesis(genesisSpec) { for _, tx := range getPseudoTransactionForGenesis(genesisSpec) {
if b32Addr, err = internalCommon.AddressToBech32(*tx.To()); err != nil { b32Addr, _ = internalCommon.AddressToBech32(*tx.To())
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
if targetB32Addr == b32Addr { if targetB32Addr == b32Addr {
accID, rosettaError := newAccountIdentifier(*tx.To()) accID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil { if rosettaError != nil {
@ -538,22 +675,15 @@ func formatGenesisTransaction(
return nil, &common.TransactionNotFoundError return nil, &common.TransactionNotFoundError
} }
// formatPreStakingBlockRewardsTransaction for block rewards in pre-staking era for a given Bech-32 address // formatPreStakingRewardTransaction for block rewards in pre-staking era for a given Bech-32 address.
func formatPreStakingBlockRewardsTransaction( func formatPreStakingRewardTransaction(
b32Address string, blockSigInfo *blockSignerInfo, txID *types.TransactionIdentifier, blockSigInfo *blockSignerInfo, address ethcommon.Address,
) (*types.Transaction, *types.Error) { ) (*types.Transaction, *types.Error) {
addr, err := internalCommon.Bech32ToAddress(b32Address) signatures, ok := blockSigInfo.signers[address]
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
signatures, ok := blockSigInfo.signers[addr]
if !ok || len(signatures) == 0 { if !ok || len(signatures) == 0 {
return nil, &common.TransactionNotFoundError return nil, &common.TransactionNotFoundError
} }
accID, rosettaError := newAccountIdentifier(addr) accID, rosettaError := newAccountIdentifier(address)
if rosettaError != nil { if rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
@ -573,11 +703,11 @@ func formatPreStakingBlockRewardsTransaction(
last = cur last = cur
i++ i++
} }
if sigAddr == addr { if sigAddr == address {
rewardsForThisBlock = rewardsForThisAddr rewardsForThisBlock = rewardsForThisAddr
if !(rewardsForThisAddr.Cmp(big.NewInt(0)) > 0) { if !(rewardsForThisAddr.Cmp(big.NewInt(0)) > 0) {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{ return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": "expected non-zero block reward in pre-staking ear for block signer", "message": "expected non-zero block reward in pre-staking era for block signer",
}) })
} }
break break
@ -585,13 +715,13 @@ func formatPreStakingBlockRewardsTransaction(
} }
return &types.Transaction{ return &types.Transaction{
TransactionIdentifier: getSpecialCaseTransactionIdentifier(blockSigInfo.blockHash, b32Address), TransactionIdentifier: txID,
Operations: []*types.Operation{ Operations: []*types.Operation{
{ {
OperationIdentifier: &types.OperationIdentifier{ OperationIdentifier: &types.OperationIdentifier{
Index: 0, Index: 0,
}, },
Type: common.PreStakingEraBlockRewardOperation, Type: common.PreStakingBlockRewardOperation,
Status: common.SuccessOperationStatus.Status, Status: common.SuccessOperationStatus.Status,
Account: accID, Account: accID,
Amount: &types.Amount{ Amount: &types.Amount{
@ -603,6 +733,38 @@ func formatPreStakingBlockRewardsTransaction(
}, nil }, nil
} }
// formatUndelegationPayoutTransaction for undelegation payouts at committee selection block
func formatUndelegationPayoutTransaction(
txID *types.TransactionIdentifier, delegatorPayouts hmy.UndelegationPayouts, address ethcommon.Address,
) (*types.Transaction, *types.Error) {
accID, rosettaError := newAccountIdentifier(address)
if rosettaError != nil {
return nil, rosettaError
}
payout, ok := delegatorPayouts[address]
if !ok {
return nil, &common.TransactionNotFoundError
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.UndelegationPayoutOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", payout),
Currency: &common.Currency,
},
},
},
}, nil
}
// formatTransaction for staking, cross-shard sender, and plain transactions // formatTransaction for staking, cross-shard sender, and plain transactions
func formatTransaction( func formatTransaction(
tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt, tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt,
@ -732,7 +894,7 @@ func getStakingOperations(
}) })
} }
// Set correct amount depending on staking message directive // Set correct amount depending on staking message directive that apply balance changes INSTANTLY
var amount *types.Amount var amount *types.Amount
switch tx.StakingType() { switch tx.StakingType() {
case stakingTypes.DirectiveCreateValidator: case stakingTypes.DirectiveCreateValidator:
@ -740,11 +902,7 @@ func getStakingOperations(
return nil, rosettaError return nil, rosettaError
} }
case stakingTypes.DirectiveDelegate: case stakingTypes.DirectiveDelegate:
if amount, rosettaError = getAmountFromDelegateMessage(tx.Data()); rosettaError != nil { if amount, rosettaError = getAmountFromDelegateMessage(receipt, tx.Data()); rosettaError != nil {
return nil, rosettaError
}
case stakingTypes.DirectiveUndelegate:
if amount, rosettaError = getAmountFromUndelegateMessage(tx.Data()); rosettaError != nil {
return nil, rosettaError return nil, rosettaError
} }
case stakingTypes.DirectiveCollectRewards: case stakingTypes.DirectiveCollectRewards:
@ -753,7 +911,7 @@ func getStakingOperations(
} }
default: default:
amount = &types.Amount{ amount = &types.Amount{
Value: fmt.Sprintf("-%v", tx.Value()), Value: "0", // All other staking transactions do not apply balance changes instantly or at all
Currency: &common.Currency, Currency: &common.Currency,
} }
} }
@ -792,7 +950,7 @@ func getAmountFromCreateValidatorMessage(data []byte) (*types.Amount, *types.Err
}, nil }, nil
} }
func getAmountFromDelegateMessage(data []byte) (*types.Amount, *types.Error) { func getAmountFromDelegateMessage(receipt *hmytypes.Receipt, data []byte) (*types.Amount, *types.Error) {
msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveDelegate) msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveDelegate)
if err != nil { if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{ return nil, common.NewError(common.CatchAllError, map[string]interface{}{
@ -805,8 +963,21 @@ func getAmountFromDelegateMessage(data []byte) (*types.Amount, *types.Error) {
"message": "unable to parse staking message for delegate tx", "message": "unable to parse staking message for delegate tx",
}) })
} }
stkAmount := stkMsg.Amount
logs := findLogsWithTopic(receipt, staking.DelegateTopic)
for _, log := range logs {
if len(log.Data) > ethcommon.AddressLength {
validatorAddress := ethcommon.BytesToAddress(log.Data[:ethcommon.AddressLength])
if log.Address == stkMsg.DelegatorAddress && stkMsg.ValidatorAddress == validatorAddress {
// Remove re-delegation amount as funds were never credited to account's balance.
stkAmount = new(big.Int).Sub(stkAmount, new(big.Int).SetBytes(log.Data[ethcommon.AddressLength:]))
break
}
}
}
return &types.Amount{ return &types.Amount{
Value: fmt.Sprintf("-%v", stkMsg.Amount), Value: fmt.Sprintf("-%v", stkAmount),
Currency: &common.Currency, Currency: &common.Currency,
}, nil }, nil
} }
@ -1018,12 +1189,7 @@ type AccountMetadata struct {
func newAccountIdentifier( func newAccountIdentifier(
address ethcommon.Address, address ethcommon.Address,
) (*types.AccountIdentifier, *types.Error) { ) (*types.AccountIdentifier, *types.Error) {
b32Address, err := internalCommon.AddressToBech32(address) b32Address, _ := internalCommon.AddressToBech32(address)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
metadata, err := rpc.NewStructuredResponse(AccountMetadata{Address: address.String()}) metadata, err := rpc.NewStructuredResponse(AccountMetadata{Address: address.String()})
if err != nil { if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{ return nil, common.NewError(common.CatchAllError, map[string]interface{}{

@ -14,6 +14,7 @@ import (
"github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core"
hmytypes "github.com/harmony-one/harmony/core/types" hmytypes "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/crypto/bls" "github.com/harmony-one/harmony/crypto/bls"
"github.com/harmony-one/harmony/hmy"
internalCommon "github.com/harmony-one/harmony/internal/common" internalCommon "github.com/harmony-one/harmony/internal/common"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node" nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
"github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/internal/params"
@ -224,12 +225,8 @@ func TestFormatGenesisTransaction(t *testing.T) {
genesisSpec := getGenesisSpec(0) genesisSpec := getGenesisSpec(0)
testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
for acc := range genesisSpec.Alloc { for acc := range genesisSpec.Alloc {
b32Addr, err := internalCommon.AddressToBech32(acc) txID := getSpecialCaseTransactionIdentifier(testBlkHash, acc, SpecialGenesisTxID)
if err != nil { tx, rosettaError := formatGenesisTransaction(txID, acc, 0)
t.Fatal(err)
}
txID := getSpecialCaseTransactionIdentifier(testBlkHash, b32Addr)
tx, rosettaError := formatGenesisTransaction(txID, b32Addr, 0)
if rosettaError != nil { if rosettaError != nil {
t.Fatal(rosettaError) t.Fatal(rosettaError)
} }
@ -251,16 +248,12 @@ func TestFormatGenesisTransaction(t *testing.T) {
} }
} }
func TestFormatPreStakingBlockRewardsTransactionSuccess(t *testing.T) { func TestFormatPreStakingRewardTransactionSuccess(t *testing.T) {
testKey, err := crypto.GenerateKey() testKey, err := crypto.GenerateKey()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
testB32Addr, err := internalCommon.AddressToBech32(testAddr)
if err != nil {
t.Fatal(err)
}
testBlockSigInfo := &blockSignerInfo{ testBlockSigInfo := &blockSignerInfo{
signers: map[ethcommon.Address][]bls.SerializedPublicKey{ signers: map[ethcommon.Address][]bls.SerializedPublicKey{
testAddr: { // Only care about length for this test testAddr: { // Only care about length for this test
@ -271,8 +264,8 @@ func TestFormatPreStakingBlockRewardsTransactionSuccess(t *testing.T) {
totalKeysSigned: 150, totalKeysSigned: 150,
blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"),
} }
refTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testB32Addr) refTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testAddr, SpecialPreStakingRewardTxID)
tx, rosettaError := formatPreStakingBlockRewardsTransaction(testB32Addr, testBlockSigInfo) tx, rosettaError := formatPreStakingRewardTransaction(refTxID, testBlockSigInfo, testAddr)
if rosettaError != nil { if rosettaError != nil {
t.Fatal(rosettaError) t.Fatal(rosettaError)
} }
@ -286,8 +279,8 @@ func TestFormatPreStakingBlockRewardsTransactionSuccess(t *testing.T) {
if tx.Operations[0].OperationIdentifier.Index != 0 { if tx.Operations[0].OperationIdentifier.Index != 0 {
t.Error("expected operational ID to be 0") t.Error("expected operational ID to be 0")
} }
if tx.Operations[0].Type != common.PreStakingEraBlockRewardOperation { if tx.Operations[0].Type != common.PreStakingBlockRewardOperation {
t.Error("expected operation type to be pre staking era block rewards") t.Error("expected operation type to be pre-staking era block rewards")
} }
if tx.Operations[0].Status != common.SuccessOperationStatus.Status { if tx.Operations[0].Status != common.SuccessOperationStatus.Status {
t.Error("expected successful operation status") t.Error("expected successful operation status")
@ -301,16 +294,12 @@ func TestFormatPreStakingBlockRewardsTransactionSuccess(t *testing.T) {
} }
} }
func TestFormatPreStakingBlockRewardsTransactionFail(t *testing.T) { func TestFormatPreStakingRewardTransactionFail(t *testing.T) {
testKey, err := crypto.GenerateKey() testKey, err := crypto.GenerateKey()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
testB32Addr, err := internalCommon.AddressToBech32(testAddr)
if err != nil {
t.Fatal(err)
}
testBlockSigInfo := &blockSignerInfo{ testBlockSigInfo := &blockSignerInfo{
signers: map[ethcommon.Address][]bls.SerializedPublicKey{ signers: map[ethcommon.Address][]bls.SerializedPublicKey{
testAddr: {}, testAddr: {},
@ -318,7 +307,8 @@ func TestFormatPreStakingBlockRewardsTransactionFail(t *testing.T) {
totalKeysSigned: 150, totalKeysSigned: 150,
blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"),
} }
_, rosettaError := formatPreStakingBlockRewardsTransaction(testB32Addr, testBlockSigInfo) testTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testAddr, SpecialPreStakingRewardTxID)
_, rosettaError := formatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr)
if rosettaError == nil { if rosettaError == nil {
t.Fatal("expected rosetta error") t.Fatal("expected rosetta error")
} }
@ -331,7 +321,7 @@ func TestFormatPreStakingBlockRewardsTransactionFail(t *testing.T) {
totalKeysSigned: 150, totalKeysSigned: 150,
blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"),
} }
_, rosettaError = formatPreStakingBlockRewardsTransaction(testB32Addr, testBlockSigInfo) _, rosettaError = formatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr)
if rosettaError == nil { if rosettaError == nil {
t.Fatal("expected rosetta error") t.Fatal("expected rosetta error")
} }
@ -340,6 +330,48 @@ func TestFormatPreStakingBlockRewardsTransactionFail(t *testing.T) {
} }
} }
func TestFormatUndelegationPayoutTransaction(t *testing.T) {
testKey, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
testPayout := big.NewInt(1e10)
testDelegatorPayouts := hmy.UndelegationPayouts{
testAddr: testPayout,
}
testBlockHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
testTxID := getSpecialCaseTransactionIdentifier(testBlockHash, testAddr, SpecialUndelegationPayoutTxID)
tx, rosettaError := formatUndelegationPayoutTransaction(testTxID, testDelegatorPayouts, testAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if len(tx.Operations) != 1 {
t.Fatal("expected tx operations to be of length 1")
}
if tx.Operations[0].OperationIdentifier.Index != 0 {
t.Error("Expect first operation to be index 0")
}
if tx.Operations[0].Type != common.UndelegationPayoutOperation {
t.Errorf("Expect operation type to be: %v", common.UndelegationPayoutOperation)
}
if tx.Operations[0].Status != common.SuccessOperationStatus.Status {
t.Error("expected successful operation status")
}
if tx.Operations[0].Amount.Value != fmt.Sprintf("%v", testPayout) {
t.Errorf("expect payout to be %v", testPayout)
}
_, rosettaError = formatUndelegationPayoutTransaction(testTxID, hmy.UndelegationPayouts{}, testAddr)
if rosettaError == nil {
t.Fatal("Expect error for no payouts found")
}
if rosettaError.Code != common.TransactionNotFoundError.Code {
t.Errorf("expect error code %v", common.TransactionNotFoundError.Code)
}
}
func testFormatCrossShardSenderTransaction( func testFormatCrossShardSenderTransaction(
t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey, t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey,
) { ) {
@ -564,7 +596,7 @@ func TestGetStakingOperationsFromUndelegate(t *testing.T) {
Status: common.SuccessOperationStatus.Status, Status: common.SuccessOperationStatus.Status,
Account: senderAccID, Account: senderAccID,
Amount: &types.Amount{ Amount: &types.Amount{
Value: fmt.Sprintf("%v", tenOnes.Uint64()), Value: fmt.Sprintf("0"),
Currency: &common.Currency, Currency: &common.Currency,
}, },
Metadata: metadata, Metadata: metadata,
@ -679,7 +711,7 @@ func TestGetStakingOperationsFromEditValidator(t *testing.T) {
Status: common.SuccessOperationStatus.Status, Status: common.SuccessOperationStatus.Status,
Account: senderAccID, Account: senderAccID,
Amount: &types.Amount{ Amount: &types.Amount{
Value: fmt.Sprintf("-%v", 0), Value: fmt.Sprintf("0"),
Currency: &common.Currency, Currency: &common.Currency,
}, },
Metadata: metadata, Metadata: metadata,
@ -1146,26 +1178,31 @@ func TestGetPseudoTransactionForGenesis(t *testing.T) {
func TestSpecialCaseTransactionIdentifier(t *testing.T) { func TestSpecialCaseTransactionIdentifier(t *testing.T) {
testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
testB32Address := "one10g7kfque6ew2jjfxxa6agkdwk4wlyjuncp6gwz" testB32Address := "one10g7kfque6ew2jjfxxa6agkdwk4wlyjuncp6gwz"
testAddress := internalCommon.MustBech32ToAddress(testB32Address)
refTxID := &types.TransactionIdentifier{ refTxID := &types.TransactionIdentifier{
Hash: fmt.Sprintf("%v_%v", testBlkHash.String(), testB32Address), Hash: fmt.Sprintf("%v_%v_%v", testBlkHash.String(), testB32Address, SpecialGenesisTxID.String()),
} }
specialTxID := getSpecialCaseTransactionIdentifier(testBlkHash, testB32Address) specialTxID := getSpecialCaseTransactionIdentifier(
testBlkHash, testAddress, SpecialGenesisTxID,
)
if !reflect.DeepEqual(refTxID, specialTxID) { if !reflect.DeepEqual(refTxID, specialTxID) {
t.Fatal("invalid for mate for special case TxID") t.Fatal("invalid for mate for special case TxID")
} }
unpackedBlkHash, unpackedB32Address, rosettaError := unpackSpecialCaseTransactionIdentifier(specialTxID) unpackedBlkHash, unpackedAddress, rosettaError := unpackSpecialCaseTransactionIdentifier(
specialTxID, SpecialGenesisTxID,
)
if rosettaError != nil { if rosettaError != nil {
t.Fatal(rosettaError) t.Fatal(rosettaError)
} }
if unpackedB32Address != testB32Address { if unpackedAddress != testAddress {
t.Errorf("expected unpacked address to be %v not %v", testB32Address, unpackedB32Address) t.Errorf("expected unpacked address to be %v not %v", testAddress.String(), unpackedAddress.String())
} }
if unpackedBlkHash.String() != testBlkHash.String() { if unpackedBlkHash.String() != testBlkHash.String() {
t.Errorf("expected blk hash to be %v not %v", unpackedBlkHash.String(), testBlkHash.String()) t.Errorf("expected blk hash to be %v not %v", unpackedBlkHash.String(), testBlkHash.String())
} }
_, _, rosettaError = unpackSpecialCaseTransactionIdentifier( _, _, rosettaError = unpackSpecialCaseTransactionIdentifier(
&types.TransactionIdentifier{Hash: ""}, &types.TransactionIdentifier{Hash: ""}, SpecialGenesisTxID,
) )
if rosettaError == nil { if rosettaError == nil {
t.Fatal("expected rosetta error") t.Fatal("expected rosetta error")

@ -80,12 +80,35 @@ func (s *NetworkAPI) NetworkStatus(
} }
stage := syncStatus.String() stage := syncStatus.String()
currentBlockIdentifier := &types.BlockIdentifier{
Index: currentHeader.Number().Int64(),
Hash: currentHeader.Hash().String(),
}
// Only applicable to non-archival nodes
var oldestBlockIdentifier *types.BlockIdentifier
if !nodeconfig.GetDefaultConfig().GetArchival() {
maxGarbCollectedBlockNum := s.hmy.BlockChain.GetMaxGarbageCollectedBlockNumber()
if maxGarbCollectedBlockNum == -1 || maxGarbCollectedBlockNum >= currentHeader.Number().Int64() {
oldestBlockIdentifier = currentBlockIdentifier
} else {
oldestBlockHeader, err := s.hmy.HeaderByNumber(ctx, rpc.BlockNumber(maxGarbCollectedBlockNum+1))
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": fmt.Sprintf("unable to get oldest block header: %v", err.Error()),
})
}
oldestBlockIdentifier = &types.BlockIdentifier{
Index: oldestBlockHeader.Number().Int64(),
Hash: oldestBlockHeader.Hash().String(),
}
}
}
return &types.NetworkStatusResponse{ return &types.NetworkStatusResponse{
CurrentBlockIdentifier: &types.BlockIdentifier{ CurrentBlockIdentifier: currentBlockIdentifier,
Index: currentHeader.Number().Int64(), OldestBlockIdentifier: oldestBlockIdentifier,
Hash: currentHeader.Hash().String(), CurrentBlockTimestamp: currentHeader.Time().Int64() * 1e3, // Timestamp must be in ms.
},
CurrentBlockTimestamp: currentHeader.Time().Int64() * 1e3, // Timestamp must be in ms.
GenesisBlockIdentifier: &types.BlockIdentifier{ GenesisBlockIdentifier: &types.BlockIdentifier{
Index: genesisHeader.Number().Int64(), Index: genesisHeader.Number().Int64(),
Hash: genesisHeader.Hash().String(), Hash: genesisHeader.Hash().String(),

Loading…
Cancel
Save