From e74ab0bc0aa2d2fd39d7dadaa96ca5a5d4777fe9 Mon Sep 17 00:00:00 2001 From: Daniel Van Der Maden Date: Fri, 11 Sep 2020 12:49:45 -0700 Subject: [PATCH] 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 * [hmy] Add GetUndelegationChange Signed-off-by: Daniel Van Der Maden * [hmy] Add GetAllUndelegatedDelegators Signed-off-by: Daniel Van Der Maden * [hmy] Fix GetAllUndelegatedDelegators & add GetDelegationLockingPeriodInEpoch Signed-off-by: Daniel Van Der Maden * [rosetta] Fix block reward TX ID formatting Signed-off-by: Daniel Van Der Maden * [hmy] Remove unused GetUndelegationChange * Fix GetUndelegationPayouts * Add GetDelegationsByValidatorAtBlock * Keep beaconchain usage at a minimum Signed-off-by: Daniel Van Der Maden * [hmy] Remove debug print & update comments for GetUndelegationPayouts Signed-off-by: Daniel Van Der Maden * [core] Add last garbage collected number to blockchain.go Signed-off-by: Daniel Van Der Maden * [rosetta] Add oldest block ID in net stat for non-archival nodes Signed-off-by: Daniel Van Der Maden * [rosetta] Fix network oldest block case when garb col blk unknown Signed-off-by: Daniel Van Der Maden * [core] Rename lastGarbCollectedBlkNum to maxGarbCollectedBlkNum Signed-off-by: Daniel Van Der Maden * [internal/chain] Refactor token lock period getter & expose * Use refactored token lock period getter Signed-off-by: Daniel Van Der Maden * [hmy] Add UndelegationPayouts type Signed-off-by: Daniel Van Der Maden * [rosetta] Improve NewError detail failure message * Add UndelegationPayoutOperation * Rename PreStakingEraBlockRewardOperation to UndelegationPayoutOperation Signed-off-by: Daniel Van Der Maden * [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 * [hmy] Add caching to GetUndelegationPayouts Signed-off-by: Daniel Van Der Maden * [hmy] Nit - fix comment Signed-off-by: Daniel Van Der Maden * [rosetta] Add block not found error Signed-off-by: Daniel Van Der Maden * [rosetta] Refactor special case txID to be for general Signed-off-by: Daniel Van Der Maden * [rosetta] Fix lint Signed-off-by: Daniel Van Der Maden * [rosetta] Nit - fix comment Signed-off-by: Daniel Van Der Maden * [hmy] Nit - Make GetUndelegationPayouts more readable Signed-off-by: Daniel Van Der Maden --- core/blockchain.go | 32 ++- hmy/hmy.go | 39 +-- hmy/staking.go | 84 ++++++- internal/chain/engine.go | 18 +- rosetta/common/errors.go | 13 +- rosetta/common/operations.go | 10 +- rosetta/common/operations_test.go | 3 +- rosetta/services/block.go | 404 +++++++++++++++++++++--------- rosetta/services/block_test.go | 97 ++++--- rosetta/services/network.go | 33 ++- 10 files changed, 532 insertions(+), 201 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index c6223aace..7e26e6026 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -161,13 +161,14 @@ type BlockChain struct { procInterrupt int32 // interrupt signaler for block processing wg sync.WaitGroup // chain processing wait group for shutting down - engine consensus_engine.Engine - processor Processor // block processor interface - validator Validator // block and state validator interface - vmConfig vm.Config - badBlocks *lru.Cache // Bad block cache - shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. - pendingSlashes slash.Records + engine consensus_engine.Engine + processor Processor // block processor interface + validator Validator // block and state validator interface + vmConfig vm.Config + badBlocks *lru.Cache // Bad block cache + shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. + pendingSlashes slash.Records + maxGarbCollectedBlkNum int64 } // NewBlockChain returns a fully initialised block chain using information @@ -228,6 +229,7 @@ func NewBlockChain( vmConfig: vmConfig, badBlocks: badBlocks, pendingSlashes: slash.Records{}, + maxGarbCollectedBlkNum: -1, } bc.SetValidator(NewBlockValidator(chainConfig, bc, engine)) bc.SetProcessor(NewStateProcessor(chainConfig, bc, engine)) @@ -1168,6 +1170,9 @@ func (bc *BlockChain) WriteBlockWithState( bc.triegc.Push(root, number) break } + if -number > bc.maxGarbCollectedBlkNum { + bc.maxGarbCollectedBlkNum = -number + } triedb.Dereference(root.(common.Hash)) } } @@ -1202,6 +1207,11 @@ func (bc *BlockChain) WriteBlockWithState( 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 // 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 @@ -2021,10 +2031,10 @@ func (bc *BlockChain) ReadPendingCrossLinks() ([]types.CrossLink, error) { // WritePendingCrossLinks saves the pending crosslinks func (bc *BlockChain) WritePendingCrossLinks(crossLinks []types.CrossLink) error { // deduplicate crosslinks if any - m := map[uint32]map[uint64](types.CrossLink){} + m := map[uint32]map[uint64]types.CrossLink{} for _, cl := range crossLinks { 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 } @@ -2111,10 +2121,10 @@ func (bc *BlockChain) DeleteFromPendingCrossLinks(crossLinks []types.CrossLink) return 0, err } - m := map[uint32]map[uint64](struct{}){} + m := map[uint32]map[uint64]struct{}{} for _, cl := range crossLinks { if _, ok := m[cl.ShardID()]; !ok { - m[cl.ShardID()] = map[uint64](struct{}){} + m[cl.ShardID()] = map[uint64]struct{}{} } m[cl.ShardID()][cl.BlockNum()] = struct{}{} } diff --git a/hmy/hmy.go b/hmy/hmy.go index 34fedbbdb..798700ffa 100644 --- a/hmy/hmy.go +++ b/hmy/hmy.go @@ -29,9 +29,10 @@ 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 - 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 + totalStakeCacheDuration = 20 // number of blocks where the returned total stake will remain the same ) var ( @@ -64,6 +65,8 @@ type Harmony struct { group singleflight.Group // leaderCache to save on recomputation every epoch. leaderCache *lru.Cache + // undelegationPayoutsCache to save on recomputation every epoch + undelegationPayoutsCache *lru.Cache // totalStakeCache to save on recomputation for `totalStakeCacheDuration` blocks. totalStakeCache *totalStakeCache } @@ -98,24 +101,26 @@ func New( ) *Harmony { chainDb := nodeAPI.Blockchain().ChainDB() leaderCache, _ := lru.New(leaderCacheSize) + undelegationPayoutsCache, _ := lru.New(undelegationPayoutsCacheSize) 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, + 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, } } diff --git a/hmy/staking.go b/hmy/staking.go index aefaf101c..884a88feb 100644 --- a/hmy/staking.go +++ b/hmy/staking.go @@ -7,10 +7,12 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core/rawdb" "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" commonRPC "github.com/harmony-one/harmony/rpc/common" "github.com/harmony-one/harmony/shard" @@ -22,7 +24,8 @@ import ( ) var ( - zero = numeric.ZeroDec() + zero = numeric.ZeroDec() + bigZero = big.NewInt(0) ) func (hmy *Harmony) readAndUpdateRawStakes( @@ -125,6 +128,16 @@ func (hmy *Harmony) IsStakingEpoch(epoch *big.Int) bool { 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 func (hmy *Harmony) SendStakingTx(ctx context.Context, signedStakingTx *staking.StakingTransaction) error { stx, _, _, _ := rawdb.ReadStakingTransaction(hmy.chainDb, signedStakingTx.Hash()) @@ -252,7 +265,7 @@ func (hmy *Harmony) GetValidatorInformation( bc := hmy.BlockChain wrapper, err := bc.ReadValidatorInformationAt(addr, block.Root()) 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) } @@ -433,6 +446,21 @@ func (hmy *Harmony) GetDelegationsByValidator(validator common.Address) []*staki 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 func (hmy *Harmony) GetDelegationsByDelegator( delegator common.Address, @@ -471,6 +499,56 @@ func (hmy *Harmony) GetDelegationsByDelegatorByBlock( 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 .. func (hmy *Harmony) GetTotalStakingSnapshot() *big.Int { if stake := hmy.totalStakeCache.pop(hmy.CurrentBlock().NumberU64()); stake != nil { diff --git a/internal/chain/engine.go b/internal/chain/engine.go index 726f738ae..a8fb611d9 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -292,12 +292,7 @@ func payoutUndelegations( "[Finalize] failed to get validator from state to finalize", ) } - lockPeriod := staking.LockPeriodInEpoch - if chain.Config().IsRedelegation(header.Epoch()) { - lockPeriod = staking.LockPeriodInEpoch - } else if chain.Config().IsQuickUnlock(header.Epoch()) { - lockPeriod = staking.LockPeriodInEpochV2 - } + lockPeriod := GetLockPeriodInEpoch(chain, header.Epoch()) for i := range wrapper.Delegations { delegation := &wrapper.Delegations[i] totalWithdraw := delegation.RemoveUnlockedUndelegations( @@ -547,3 +542,14 @@ func GetPublicKeys( } 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 +} diff --git a/rosetta/common/errors.go b/rosetta/common/errors.go index 2caeaf4ec..4a1907f9e 100644 --- a/rosetta/common/errors.go +++ b/rosetta/common/errors.go @@ -1,6 +1,8 @@ package common import ( + "fmt" + "github.com/coinbase/rosetta-sdk-go/types" "github.com/harmony-one/harmony/rpc" ) @@ -65,15 +67,14 @@ var ( // NewError create a new error with a given detail structure func NewError(rosettaError types.Error, detailStructure interface{}) *types.Error { + newError := rosettaError details, err := rpc.NewStructuredResponse(detailStructure) if err != nil { - newError := CatchAllError - CatchAllError.Details = map[string]interface{}{ - "message": err.Error(), + newError.Details = map[string]interface{}{ + "message": fmt.Sprintf("unable to get error details: %v", err.Error()), } - return &newError + } else { + newError.Details = details } - newError := rosettaError - newError.Details = details return &newError } diff --git a/rosetta/common/operations.go b/rosetta/common/operations.go index 269d13793..344241f79 100644 --- a/rosetta/common/operations.go +++ b/rosetta/common/operations.go @@ -21,8 +21,11 @@ const ( // GenesisFundsOperation .. GenesisFundsOperation = "Genesis" - // PreStakingEraBlockRewardOperation .. - PreStakingEraBlockRewardOperation = "PreStakingBlockReward" + // PreStakingBlockRewardOperation .. + PreStakingBlockRewardOperation = "PreOpenStakingBlockReward" + + // UndelegationPayoutOperation .. + UndelegationPayoutOperation = "UndelegationPayout" ) var ( @@ -33,7 +36,8 @@ var ( CrossShardTransferOperation, ContractCreationOperation, GenesisFundsOperation, - PreStakingEraBlockRewardOperation, + PreStakingBlockRewardOperation, + UndelegationPayoutOperation, } // StakingOperationTypes .. diff --git a/rosetta/common/operations_test.go b/rosetta/common/operations_test.go index 9a43c8a18..a5d4465da 100644 --- a/rosetta/common/operations_test.go +++ b/rosetta/common/operations_test.go @@ -54,7 +54,8 @@ func TestPlainOperationTypes(t *testing.T) { CrossShardTransferOperation, ContractCreationOperation, GenesisFundsOperation, - PreStakingEraBlockRewardOperation, + PreStakingBlockRewardOperation, + UndelegationPayoutOperation, } sort.Strings(referenceOperationTypes) sort.Strings(plainOperationTypes) diff --git a/rosetta/services/block.go b/rosetta/services/block.go index c2329b779..8963776f0 100644 --- a/rosetta/services/block.go +++ b/rosetta/services/block.go @@ -29,10 +29,6 @@ import ( stakingTypes "github.com/harmony-one/harmony/staking/types" ) -const ( - blockHashLen = 64 -) - // BlockAPI implements the server.BlockAPIServicer interface. type BlockAPI struct { hmy *hmy.Harmony @@ -79,11 +75,18 @@ func (s *BlockAPI) Block( 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{ BlockIdentifier: currBlockID, ParentBlockIdentifier: prevBlockID, 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{} @@ -99,6 +102,7 @@ func (s *BlockAPI) Block( } // Report cross-shard transaction payouts. for _, cxReceipts := range blk.IncomingReceipts() { + // Report cross-shard transaction payouts. for _, cxReceipt := range cxReceipts.Receipts { otherTransactions = append(otherTransactions, &types.TransactionIdentifier{ Hash: cxReceipt.TxHash.String(), @@ -107,21 +111,11 @@ func (s *BlockAPI) Block( } // Report pre-staking era block rewards as transactions to fit API. if !s.hmy.IsStakingEpoch(blk.Epoch()) { - blockSigInfo, rosettaError := getBlockSignerInfo(ctx, s.hmy, blk) + preStakingRewardTxIDs, rosettaError := s.getAllPreStakingRewardTransactionIdentifiers(ctx, blk) if rosettaError != nil { return nil, rosettaError } - for acc, signedBlsKeys := range blockSigInfo.signers { - 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)) - } - } + otherTransactions = append(otherTransactions, preStakingRewardTxIDs...) } return &types.BlockResponse{ @@ -157,13 +151,9 @@ func (s *BlockAPI) genesisBlock( otherTransactions := []*types.TransactionIdentifier{} // Report initial genesis funds as transactions to fit API. for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(blk.ShardID())) { - b32Addr, err := internalCommon.AddressToBech32(*tx.To()) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - otherTransactions = append(otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), b32Addr)) + otherTransactions = append( + otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), *tx.To(), SpecialGenesisTxID), + ) } return &types.BlockResponse{ @@ -172,10 +162,111 @@ func (s *BlockAPI) genesisBlock( }, 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 func (s *BlockAPI) BlockTransaction( 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 { return nil, err } @@ -189,17 +280,14 @@ func (s *BlockAPI) BlockTransaction( txHash := ethcommon.HexToHash(request.TransactionIdentifier.Hash) txInfo, rosettaError := s.getTransactionInfo(ctx, blockHash, txHash) if rosettaError != nil { - blk, rosettaError2 := s.getBlock(ctx, &types.PartialBlockIdentifier{Index: &request.BlockIdentifier.Index}) - if rosettaError2 != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "error": rosettaError2, - "base_error": rosettaError, + // If no transaction info is found, check for special case transactions. + response, rosettaError2 := s.specialBlockTransaction(ctx, request) + if rosettaError2 != nil && rosettaError2.Code != common.TransactionNotFoundError.Code { + return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{ + "from_error": rosettaError2, }) } - if s.hmy.IsStakingEpoch(blk.Epoch()) { - return nil, rosettaError - } - return s.preStakingEraBlockRewardTransaction(ctx, request.TransactionIdentifier, blk) + return response, rosettaError2 } var transaction *types.Transaction @@ -229,25 +317,51 @@ func (s *BlockAPI) genesisBlockTransaction( "message": err.Error(), }) } - blkHash, b32Addr, rosettaError := unpackSpecialCaseTransactionIdentifier(request.TransactionIdentifier) + blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier( + request.TransactionIdentifier, SpecialGenesisTxID, + ) if rosettaError != nil { return nil, rosettaError } if blkHash.String() != genesisBlock.Hash().String() { 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 { return nil, rosettaError } return &types.BlockTransactionResponse{Transaction: txs}, nil } -// preStakingEraBlockRewardTransaction is a special handler for pre-staking era block reward transactions -func (s *BlockAPI) preStakingEraBlockRewardTransaction( +// specialBlockTransaction is a formatter for special, non-genesis, transactions +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, ) (*types.BlockTransactionResponse, *types.Error) { - blkHash, b32Address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID) + blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialPreStakingRewardTxID) if rosettaError != nil { 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 { 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 { return nil, rosettaError } @@ -287,6 +431,9 @@ func (s *BlockAPI) getBlock( "message": err.Error(), }) } + if blk == nil { + return nil, &common.BlockNotFoundError + } return blk, nil } @@ -372,30 +519,56 @@ func getPseudoTransactionForGenesis(spec *core.Genesis) []*hmytypes.Transaction 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. -// 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. func getSpecialCaseTransactionIdentifier( - blockHash ethcommon.Hash, suffix string, + blockHash ethcommon.Hash, address ethcommon.Address, suffix SpecialTransactionSuffix, ) *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. func unpackSpecialCaseTransactionIdentifier( - txID *types.TransactionIdentifier, -) (ethcommon.Hash, string, *types.Error) { + txID *types.TransactionIdentifier, expectedSuffix SpecialTransactionSuffix, +) (ethcommon.Hash, ethcommon.Address, *types.Error) { hash := txID.Hash hash = strings.TrimPrefix(hash, "0x") hash = strings.TrimPrefix(hash, "0X") - if len(hash) < blockHashLen+1 || string(hash[blockHashLen]) != "_" { - return ethcommon.Hash{}, "", common.NewError(common.CatchAllError, map[string]interface{}{ + minCharCount := blockHashStrLen + b32AddrStrLen + 2 + 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", }) } - 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 @@ -410,38 +583,6 @@ type blockSignerInfo struct { 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 .. type TransactionMetadata struct { CrossShardIdentifier *types.TransactionIdentifier `json:"cross_shard_transaction_identifier,omitempty"` @@ -497,17 +638,13 @@ func formatCrossShardReceiverTransaction( // formatGenesisTransaction for genesis block's initial balances func formatGenesisTransaction( - txID *types.TransactionIdentifier, targetB32Addr string, shardID uint32, + txID *types.TransactionIdentifier, targetAddr ethcommon.Address, shardID uint32, ) (fmtTx *types.Transaction, rosettaError *types.Error) { var b32Addr string - var err error + targetB32Addr := internalCommon.MustAddressToBech32(targetAddr) genesisSpec := getGenesisSpec(shardID) for _, tx := range getPseudoTransactionForGenesis(genesisSpec) { - if b32Addr, err = internalCommon.AddressToBech32(*tx.To()); err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } + b32Addr, _ = internalCommon.AddressToBech32(*tx.To()) if targetB32Addr == b32Addr { accID, rosettaError := newAccountIdentifier(*tx.To()) if rosettaError != nil { @@ -538,22 +675,15 @@ func formatGenesisTransaction( return nil, &common.TransactionNotFoundError } -// formatPreStakingBlockRewardsTransaction for block rewards in pre-staking era for a given Bech-32 address -func formatPreStakingBlockRewardsTransaction( - b32Address string, blockSigInfo *blockSignerInfo, +// formatPreStakingRewardTransaction for block rewards in pre-staking era for a given Bech-32 address. +func formatPreStakingRewardTransaction( + txID *types.TransactionIdentifier, blockSigInfo *blockSignerInfo, address ethcommon.Address, ) (*types.Transaction, *types.Error) { - addr, err := internalCommon.Bech32ToAddress(b32Address) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - - signatures, ok := blockSigInfo.signers[addr] + signatures, ok := blockSigInfo.signers[address] if !ok || len(signatures) == 0 { return nil, &common.TransactionNotFoundError } - accID, rosettaError := newAccountIdentifier(addr) + accID, rosettaError := newAccountIdentifier(address) if rosettaError != nil { return nil, rosettaError } @@ -573,11 +703,11 @@ func formatPreStakingBlockRewardsTransaction( last = cur i++ } - if sigAddr == addr { + if sigAddr == 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 ear for block signer", + "message": "expected non-zero block reward in pre-staking era for block signer", }) } break @@ -585,13 +715,13 @@ func formatPreStakingBlockRewardsTransaction( } return &types.Transaction{ - TransactionIdentifier: getSpecialCaseTransactionIdentifier(blockSigInfo.blockHash, b32Address), + TransactionIdentifier: txID, Operations: []*types.Operation{ { OperationIdentifier: &types.OperationIdentifier{ Index: 0, }, - Type: common.PreStakingEraBlockRewardOperation, + Type: common.PreStakingBlockRewardOperation, Status: common.SuccessOperationStatus.Status, Account: accID, Amount: &types.Amount{ @@ -603,6 +733,38 @@ func formatPreStakingBlockRewardsTransaction( }, 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 func formatTransaction( 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 switch tx.StakingType() { case stakingTypes.DirectiveCreateValidator: @@ -740,11 +902,7 @@ func getStakingOperations( return nil, rosettaError } case stakingTypes.DirectiveDelegate: - if amount, rosettaError = getAmountFromDelegateMessage(tx.Data()); rosettaError != nil { - return nil, rosettaError - } - case stakingTypes.DirectiveUndelegate: - if amount, rosettaError = getAmountFromUndelegateMessage(tx.Data()); rosettaError != nil { + if amount, rosettaError = getAmountFromDelegateMessage(receipt, tx.Data()); rosettaError != nil { return nil, rosettaError } case stakingTypes.DirectiveCollectRewards: @@ -753,7 +911,7 @@ func getStakingOperations( } default: 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, } } @@ -792,7 +950,7 @@ func getAmountFromCreateValidatorMessage(data []byte) (*types.Amount, *types.Err }, 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) if err != nil { 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", }) } + + 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{ - Value: fmt.Sprintf("-%v", stkMsg.Amount), + Value: fmt.Sprintf("-%v", stkAmount), Currency: &common.Currency, }, nil } @@ -1018,12 +1189,7 @@ type AccountMetadata struct { func newAccountIdentifier( address ethcommon.Address, ) (*types.AccountIdentifier, *types.Error) { - b32Address, err := internalCommon.AddressToBech32(address) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } + b32Address, _ := internalCommon.AddressToBech32(address) metadata, err := rpc.NewStructuredResponse(AccountMetadata{Address: address.String()}) if err != nil { return nil, common.NewError(common.CatchAllError, map[string]interface{}{ diff --git a/rosetta/services/block_test.go b/rosetta/services/block_test.go index 70ab29e47..c3441734c 100644 --- a/rosetta/services/block_test.go +++ b/rosetta/services/block_test.go @@ -14,6 +14,7 @@ import ( "github.com/harmony-one/harmony/core" hmytypes "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/crypto/bls" + "github.com/harmony-one/harmony/hmy" internalCommon "github.com/harmony-one/harmony/internal/common" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" "github.com/harmony-one/harmony/internal/params" @@ -224,12 +225,8 @@ func TestFormatGenesisTransaction(t *testing.T) { genesisSpec := getGenesisSpec(0) testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") for acc := range genesisSpec.Alloc { - b32Addr, err := internalCommon.AddressToBech32(acc) - if err != nil { - t.Fatal(err) - } - txID := getSpecialCaseTransactionIdentifier(testBlkHash, b32Addr) - tx, rosettaError := formatGenesisTransaction(txID, b32Addr, 0) + txID := getSpecialCaseTransactionIdentifier(testBlkHash, acc, SpecialGenesisTxID) + tx, rosettaError := formatGenesisTransaction(txID, acc, 0) if rosettaError != nil { 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() if err != nil { t.Fatal(err) } testAddr := crypto.PubkeyToAddress(testKey.PublicKey) - testB32Addr, err := internalCommon.AddressToBech32(testAddr) - if err != nil { - t.Fatal(err) - } testBlockSigInfo := &blockSignerInfo{ signers: map[ethcommon.Address][]bls.SerializedPublicKey{ testAddr: { // Only care about length for this test @@ -271,8 +264,8 @@ func TestFormatPreStakingBlockRewardsTransactionSuccess(t *testing.T) { totalKeysSigned: 150, blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), } - refTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testB32Addr) - tx, rosettaError := formatPreStakingBlockRewardsTransaction(testB32Addr, testBlockSigInfo) + refTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testAddr, SpecialPreStakingRewardTxID) + tx, rosettaError := formatPreStakingRewardTransaction(refTxID, testBlockSigInfo, testAddr) if rosettaError != nil { t.Fatal(rosettaError) } @@ -286,8 +279,8 @@ func TestFormatPreStakingBlockRewardsTransactionSuccess(t *testing.T) { if tx.Operations[0].OperationIdentifier.Index != 0 { t.Error("expected operational ID to be 0") } - if tx.Operations[0].Type != common.PreStakingEraBlockRewardOperation { - t.Error("expected operation type to be pre staking era block rewards") + if tx.Operations[0].Type != common.PreStakingBlockRewardOperation { + t.Error("expected operation type to be pre-staking era block rewards") } if tx.Operations[0].Status != common.SuccessOperationStatus.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() if err != nil { t.Fatal(err) } testAddr := crypto.PubkeyToAddress(testKey.PublicKey) - testB32Addr, err := internalCommon.AddressToBech32(testAddr) - if err != nil { - t.Fatal(err) - } testBlockSigInfo := &blockSignerInfo{ signers: map[ethcommon.Address][]bls.SerializedPublicKey{ testAddr: {}, @@ -318,7 +307,8 @@ func TestFormatPreStakingBlockRewardsTransactionFail(t *testing.T) { totalKeysSigned: 150, blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), } - _, rosettaError := formatPreStakingBlockRewardsTransaction(testB32Addr, testBlockSigInfo) + testTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testAddr, SpecialPreStakingRewardTxID) + _, rosettaError := formatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr) if rosettaError == nil { t.Fatal("expected rosetta error") } @@ -331,7 +321,7 @@ func TestFormatPreStakingBlockRewardsTransactionFail(t *testing.T) { totalKeysSigned: 150, blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), } - _, rosettaError = formatPreStakingBlockRewardsTransaction(testB32Addr, testBlockSigInfo) + _, rosettaError = formatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr) if rosettaError == nil { 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( t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey, ) { @@ -564,7 +596,7 @@ func TestGetStakingOperationsFromUndelegate(t *testing.T) { Status: common.SuccessOperationStatus.Status, Account: senderAccID, Amount: &types.Amount{ - Value: fmt.Sprintf("%v", tenOnes.Uint64()), + Value: fmt.Sprintf("0"), Currency: &common.Currency, }, Metadata: metadata, @@ -679,7 +711,7 @@ func TestGetStakingOperationsFromEditValidator(t *testing.T) { Status: common.SuccessOperationStatus.Status, Account: senderAccID, Amount: &types.Amount{ - Value: fmt.Sprintf("-%v", 0), + Value: fmt.Sprintf("0"), Currency: &common.Currency, }, Metadata: metadata, @@ -1146,26 +1178,31 @@ func TestGetPseudoTransactionForGenesis(t *testing.T) { func TestSpecialCaseTransactionIdentifier(t *testing.T) { testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") testB32Address := "one10g7kfque6ew2jjfxxa6agkdwk4wlyjuncp6gwz" + testAddress := internalCommon.MustBech32ToAddress(testB32Address) 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) { t.Fatal("invalid for mate for special case TxID") } - unpackedBlkHash, unpackedB32Address, rosettaError := unpackSpecialCaseTransactionIdentifier(specialTxID) + unpackedBlkHash, unpackedAddress, rosettaError := unpackSpecialCaseTransactionIdentifier( + specialTxID, SpecialGenesisTxID, + ) if rosettaError != nil { t.Fatal(rosettaError) } - if unpackedB32Address != testB32Address { - t.Errorf("expected unpacked address to be %v not %v", testB32Address, unpackedB32Address) + if unpackedAddress != testAddress { + t.Errorf("expected unpacked address to be %v not %v", testAddress.String(), unpackedAddress.String()) } if unpackedBlkHash.String() != testBlkHash.String() { t.Errorf("expected blk hash to be %v not %v", unpackedBlkHash.String(), testBlkHash.String()) } _, _, rosettaError = unpackSpecialCaseTransactionIdentifier( - &types.TransactionIdentifier{Hash: ""}, + &types.TransactionIdentifier{Hash: ""}, SpecialGenesisTxID, ) if rosettaError == nil { t.Fatal("expected rosetta error") diff --git a/rosetta/services/network.go b/rosetta/services/network.go index 04a183bba..a698c692e 100644 --- a/rosetta/services/network.go +++ b/rosetta/services/network.go @@ -80,12 +80,35 @@ func (s *NetworkAPI) NetworkStatus( } 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{ - CurrentBlockIdentifier: &types.BlockIdentifier{ - Index: currentHeader.Number().Int64(), - Hash: currentHeader.Hash().String(), - }, - CurrentBlockTimestamp: currentHeader.Time().Int64() * 1e3, // Timestamp must be in ms. + CurrentBlockIdentifier: currentBlockIdentifier, + OldestBlockIdentifier: oldestBlockIdentifier, + CurrentBlockTimestamp: currentHeader.Time().Int64() * 1e3, // Timestamp must be in ms. GenesisBlockIdentifier: &types.BlockIdentifier{ Index: genesisHeader.Number().Int64(), Hash: genesisHeader.Hash().String(),