From b088d6dc776afe04ad18ab129c5da4f228fb5694 Mon Sep 17 00:00:00 2001 From: Daniel Van Der Maden Date: Fri, 16 Oct 2020 20:30:49 -0700 Subject: [PATCH] Rosetta Implementation Cleanup (Stage 3 of Node API Overhaul) (#3390) * [core] Add FindLogsWithTopic & unit test Signed-off-by: Daniel Van Der Maden * [hmy] Add GetDetailedBlockSignerInfo Signed-off-by: Daniel Van Der Maden * [hmy] Add IsCommitteeSelectionBlock Signed-off-by: Daniel Van Der Maden * [test] Add test transaction creation helpers Signed-off-by: Daniel Van Der Maden * [rosetta] Refactor account.go & add tests * Move TestNewAccountIdentifier & TestGetAddress to account_test.go Signed-off-by: Daniel Van Der Maden * [rosetta] Move Operation & Tx formatting to own files * Move Respective unit tests to own files * Expose GetOperations & GetStakingOperations * Expose FormatTransaction, FormatCrossShardReceiverTransaction, FormatGenesisTransaction, FormatPreStakingRewardTransaction & FormatUndelegationPayoutTransaction Signed-off-by: Daniel Van Der Maden * [rosetta] Move TransactionMetadata to transaction_construction.go Signed-off-by: Daniel Van Der Maden * [rosetta] Update construction to use new helpers & formatters * Make docs consistent for mempool.go Signed-off-by: Daniel Van Der Maden * [rosetta] Move all special tx & blk handling to own file Signed-off-by: Daniel Van Der Maden * [rosetta] Remove all moved fns, methods & tests from block.go Signed-off-by: Daniel Van Der Maden * Fix lint & imports Signed-off-by: Daniel Van Der Maden * [rosetta] Rename all tx related files for clarity Signed-off-by: Daniel Van Der Maden * [rosetta] Rename DefaultSenderAddress to FormatDefaultSenderAddress Signed-off-by: Daniel Van Der Maden * [rosetta] Rename Currency to NativeCurrency * This is in anticipation of HRC20 token support with rosetta * Rename various native operation functions accordingly * Add documentation to explain what a native token is Signed-off-by: Daniel Van Der Maden * [rosetta] Fix pre-staking block reward calculation * Move getPreStakingRewardTransactionIdentifiers to block_special.go * Add epoch to block metadata * Update unit tests Signed-off-by: Daniel Van Der Maden * Add IsLastBlockInEpoch method to Block & Header * Refactor all uses of length check `ShardState` * [hmy] Refactor IsCommitteeSelectionBlock to use chain.IsCommitteeSelectionBlock * Address PR comments Signed-off-by: Daniel Van Der Maden * [rosetta] Update var names in preStakingRewardBlockTransaction Signed-off-by: Daniel Van Der Maden --- block/header.go | 6 + consensus/consensus_service.go | 9 +- core/blockchain.go | 2 +- core/offchain.go | 4 +- core/types/block.go | 5 + core/types/receipt.go | 17 + core/types/receipt_test.go | 114 ++ hmy/blockchain.go | 45 + hmy/staking.go | 6 + internal/chain/engine.go | 12 +- node/node_explorer.go | 2 +- node/node_handler.go | 2 +- node/worker/worker.go | 2 +- rosetta/common/config.go | 20 +- rosetta/common/operations.go | 27 +- rosetta/common/operations_test.go | 4 +- rosetta/services/account.go | 44 +- rosetta/services/account_test.go | 72 + rosetta/services/block.go | 1116 +------------- rosetta/services/block_special.go | 334 +++++ rosetta/services/block_special_test.go | 77 + rosetta/services/block_test.go | 1326 ----------------- rosetta/services/construction_check.go | 12 +- rosetta/services/construction_check_test.go | 12 +- rosetta/services/construction_create_test.go | 9 +- rosetta/services/construction_parse.go | 6 +- rosetta/services/construction_parse_test.go | 13 +- rosetta/services/mempool.go | 6 +- ...ion_construction.go => tx_construction.go} | 29 +- ...uction_test.go => tx_construction_test.go} | 22 +- rosetta/services/tx_format.go | 277 ++++ rosetta/services/tx_format_test.go | 504 +++++++ rosetta/services/tx_operation.go | 385 +++++ ...mponents.go => tx_operation_components.go} | 12 +- ...est.go => tx_operation_components_test.go} | 54 +- rosetta/services/tx_operation_test.go | 594 ++++++++ test/helpers/transaction.go | 81 + 37 files changed, 2740 insertions(+), 2522 deletions(-) create mode 100644 core/types/receipt_test.go create mode 100644 rosetta/services/account_test.go create mode 100644 rosetta/services/block_special.go create mode 100644 rosetta/services/block_special_test.go delete mode 100644 rosetta/services/block_test.go rename rosetta/services/{transaction_construction.go => tx_construction.go} (84%) rename rosetta/services/{transaction_construction_test.go => tx_construction_test.go} (96%) create mode 100644 rosetta/services/tx_format.go create mode 100644 rosetta/services/tx_format_test.go create mode 100644 rosetta/services/tx_operation.go rename rosetta/services/{operation_components.go => tx_operation_components.go} (95%) rename rosetta/services/{operation_components_test.go => tx_operation_components_test.go} (92%) create mode 100644 rosetta/services/tx_operation_test.go create mode 100644 test/helpers/transaction.go diff --git a/block/header.go b/block/header.go index 6a7f79f99..844b9809e 100644 --- a/block/header.go +++ b/block/header.go @@ -116,6 +116,12 @@ func (h *Header) With() HeaderFieldSetter { return HeaderFieldSetter{h: h} } +// IsLastBlockInEpoch returns True if it is the last block of the epoch. +// Note that the last block contains the shard state of the next epoch. +func (h *Header) IsLastBlockInEpoch() bool { + return len(h.ShardState()) > 0 +} + // HeaderRegistry is the taggedrlp type registry for versioned headers. var HeaderRegistry = taggedrlp.NewRegistry() diff --git a/consensus/consensus_service.go b/consensus/consensus_service.go index ff9a07bd2..587b9a4e7 100644 --- a/consensus/consensus_service.go +++ b/consensus/consensus_service.go @@ -287,7 +287,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode { nextEpoch := new(big.Int).Add(curHeader.Epoch(), common.Big1) // Overwrite nextEpoch if the shard state has a epoch number - if len(curHeader.ShardState()) > 0 { + if curHeader.IsLastBlockInEpoch() { nextShardState, err := curHeader.GetShardState() if err != nil { return Syncing @@ -300,8 +300,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode { consensus.BlockPeriod = 5 * time.Second isFirstTimeStaking := consensus.ChainReader.Config().IsStaking(nextEpoch) && - len(curHeader.ShardState()) > 0 && - !consensus.ChainReader.Config().IsStaking(curEpoch) + curHeader.IsLastBlockInEpoch() && !consensus.ChainReader.Config().IsStaking(curEpoch) haventUpdatedDecider := consensus.ChainReader.Config().IsStaking(curEpoch) && consensus.Decider.Policy() != quorum.SuperMajorityStake @@ -331,7 +330,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode { consensus.getLogger().Info().Msg("[UpdateConsensusInformation] Updating.....") // genesis block is a special case that will have shard state and needs to skip processing isNotGenesisBlock := curHeader.Number().Cmp(big.NewInt(0)) > 0 - if len(curHeader.ShardState()) > 0 && isNotGenesisBlock { + if curHeader.IsLastBlockInEpoch() && isNotGenesisBlock { nextShardState, err := committee.WithStakingEnabled.ReadFromDB( nextEpoch, consensus.ChainReader, @@ -401,7 +400,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode { Msg("[UpdateConsensusInformation] changing committee") // take care of possible leader change during the epoch - if len(curHeader.ShardState()) == 0 && curHeader.Number().Uint64() != 0 { + if !curHeader.IsLastBlockInEpoch() && curHeader.Number().Uint64() != 0 { leaderPubKey, err := consensus.getLeaderPubKeyFromCoinbase(curHeader) if err != nil || leaderPubKey == nil { consensus.getLogger().Error().Err(err). diff --git a/core/blockchain.go b/core/blockchain.go index 9452159a5..6fd34e1c6 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1166,7 +1166,7 @@ func (bc *BlockChain) WriteBlockWithState( // Flush trie state into disk if it's archival node or the block is epoch block triedb := bc.stateCache.TrieDB() - if bc.cacheConfig.Disabled || len(block.Header().ShardState()) > 0 { + if bc.cacheConfig.Disabled || block.IsLastBlockInEpoch() { if err := triedb.Commit(root, false); err != nil { if isUnrecoverableErr(err) { fmt.Printf("Unrecoverable error when committing triedb: %v\nExitting\n", err) diff --git a/core/offchain.go b/core/offchain.go index 023ea8621..e8058a5f9 100644 --- a/core/offchain.go +++ b/core/offchain.go @@ -39,7 +39,7 @@ func (bc *BlockChain) CommitOffChainData( isStaking := bc.chainConfig.IsStaking(block.Epoch()) isPreStaking := bc.chainConfig.IsPreStaking(block.Epoch()) header := block.Header() - isNewEpoch := len(header.ShardState()) > 0 + isNewEpoch := block.IsLastBlockInEpoch() // Cross-shard txns epoch := block.Header().Epoch() if bc.chainConfig.HasCrossTxFields(block.Epoch()) { @@ -317,7 +317,7 @@ func (bc *BlockChain) writeValidatorStats( func (bc *BlockChain) getNextBlockEpoch(header *block.Header) (*big.Int, error) { nextBlockEpoch := header.Epoch() - if len(header.ShardState()) > 0 { + if header.IsLastBlockInEpoch() { nextBlockEpoch = new(big.Int).Add(header.Epoch(), common.Big1) decodeShardState, err := shard.DecodeWrapper(header.ShardState()) if err != nil { diff --git a/core/types/block.go b/core/types/block.go index e350037a0..a58820df7 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -436,6 +436,11 @@ func (b *Block) Uncles() []*block.Header { return b.uncles } +// IsLastBlockInEpoch returns if its the last block of the epoch. +func (b *Block) IsLastBlockInEpoch() bool { + return b.header.IsLastBlockInEpoch() +} + // Transactions returns transactions. func (b *Block) Transactions() Transactions { return b.transactions diff --git a/core/types/receipt.go b/core/types/receipt.go index 065c689e9..d0a6c6dc7 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -23,6 +23,7 @@ import ( "unsafe" "github.com/ethereum/go-ethereum/common" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" @@ -218,3 +219,19 @@ func (r Receipts) ToShardID(i int) uint32 { func (r Receipts) MaxToShardID() uint32 { return 0 } + +// FindLogsWithTopic returns all the logs that contain the given receipt +func FindLogsWithTopic( + receipt *Receipt, targetTopic ethcommon.Hash, +) []*Log { + logs := []*Log{} + for _, log := range receipt.Logs { + for _, topic := range log.Topics { + if topic == targetTopic { + logs = append(logs, log) + break + } + } + } + return logs +} diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go new file mode 100644 index 000000000..5a8891cf4 --- /dev/null +++ b/core/types/receipt_test.go @@ -0,0 +1,114 @@ +package types + +import ( + "reflect" + "testing" + + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/harmony-one/harmony/staking" +) + +func TestFindLogsWithTopic(t *testing.T) { + tests := []struct { + receipt *Receipt + topic ethcommon.Hash + expectedResponse []*Log + }{ + // test 0 + { + receipt: &Receipt{ + Logs: []*Log{ + { + Topics: []ethcommon.Hash{ + staking.IsValidatorKey, + staking.IsValidator, + }, + }, + { + Topics: []ethcommon.Hash{ + crypto.Keccak256Hash([]byte("test")), + }, + }, + { + Topics: []ethcommon.Hash{ + staking.CollectRewardsTopic, + }, + }, + }, + }, + topic: staking.IsValidatorKey, + expectedResponse: []*Log{ + { + Topics: []ethcommon.Hash{ + staking.IsValidatorKey, + staking.IsValidator, + }, + }, + }, + }, + // test 1 + { + receipt: &Receipt{ + Logs: []*Log{ + { + Topics: []ethcommon.Hash{ + staking.IsValidatorKey, + staking.IsValidator, + }, + }, + { + Topics: []ethcommon.Hash{ + crypto.Keccak256Hash([]byte("test")), + }, + }, + { + Topics: []ethcommon.Hash{ + staking.CollectRewardsTopic, + }, + }, + }, + }, + topic: staking.CollectRewardsTopic, + expectedResponse: []*Log{ + { + Topics: []ethcommon.Hash{ + staking.CollectRewardsTopic, + }, + }, + }, + }, + // test 2 + { + receipt: &Receipt{ + Logs: []*Log{ + { + Topics: []ethcommon.Hash{ + staking.IsValidatorKey, + }, + }, + { + Topics: []ethcommon.Hash{ + crypto.Keccak256Hash([]byte("test")), + }, + }, + { + Topics: []ethcommon.Hash{ + staking.CollectRewardsTopic, + }, + }, + }, + }, + topic: staking.IsValidator, + expectedResponse: []*Log{}, + }, + } + + for i, test := range tests { + response := FindLogsWithTopic(test.receipt, test.topic) + if !reflect.DeepEqual(test.expectedResponse, response) { + t.Errorf("Failed test %v, expected %v, got %v", i, test.expectedResponse, response) + } + } +} diff --git a/hmy/blockchain.go b/hmy/blockchain.go index e07d159b5..866504164 100644 --- a/hmy/blockchain.go +++ b/hmy/blockchain.go @@ -73,6 +73,51 @@ func (hmy *Harmony) GetBlockSigners( return committee.Slots, mask, nil } +// DetailedBlockSignerInfo contains all of the block singing information +type DetailedBlockSignerInfo struct { + // Signers is a map of addresses in the Signers for the block to + // all of the serialized BLS keys that signed said block. + Signers map[common.Address][]bls.SerializedPublicKey + // Committee when the block was signed. + Committee shard.SlotList + // TotalKeysSigned is the total number of bls keys that signed the block. + TotalKeysSigned uint + // Mask is the bitmap Mask for the block. + Mask *bls.Mask + BlockHash common.Hash +} + +// GetDetailedBlockSignerInfo fetches the block signer information for any non-genesis block +func (hmy *Harmony) GetDetailedBlockSignerInfo( + ctx context.Context, blk *types.Block, +) (*DetailedBlockSignerInfo, error) { + slotList, mask, err := hmy.GetBlockSigners( + ctx, rpc.BlockNumber(blk.Number().Uint64()), + ) + if err != nil { + return nil, err + } + + totalSigners := uint(0) + sigInfos := map[common.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 &DetailedBlockSignerInfo{ + Signers: sigInfos, + Committee: slotList, + TotalKeysSigned: totalSigners, + Mask: mask, + BlockHash: blk.Hash(), + }, nil +} + // GetLatestChainHeaders .. func (hmy *Harmony) GetLatestChainHeaders() *block.HeaderPair { return &block.HeaderPair{ diff --git a/hmy/staking.go b/hmy/staking.go index 884a88feb..8de76f985 100644 --- a/hmy/staking.go +++ b/hmy/staking.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rpc" + "github.com/harmony-one/harmony/block" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core/rawdb" "github.com/harmony-one/harmony/core/types" @@ -133,6 +134,11 @@ func (hmy *Harmony) IsPreStakingEpoch(epoch *big.Int) bool { return hmy.BlockChain.Config().IsPreStaking(epoch) } +// IsCommitteeSelectionBlock checks if the given block is the committee selection block +func (hmy *Harmony) IsCommitteeSelectionBlock(header *block.Header) bool { + return chain.IsCommitteeSelectionBlock(hmy.BlockChain, header) +} + // GetDelegationLockingPeriodInEpoch ... func (hmy *Harmony) GetDelegationLockingPeriodInEpoch(epoch *big.Int) int { return chain.GetLockPeriodInEpoch(hmy.BlockChain, epoch) diff --git a/internal/chain/engine.go b/internal/chain/engine.go index a8fb611d9..501a44ea5 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -212,13 +212,11 @@ func (e *engineImpl) Finalize( ) (*types.Block, reward.Reader, error) { isBeaconChain := header.ShardID() == shard.BeaconChainShardID - isNewEpoch := len(header.ShardState()) > 0 - inPreStakingEra := chain.Config().IsPreStaking(header.Epoch()) inStakingEra := chain.Config().IsStaking(header.Epoch()) // Process Undelegations, set LastEpochInCommittee and set EPoS status // Needs to be before AccumulateRewardsAndCountSigs - if isBeaconChain && isNewEpoch && inPreStakingEra { + if IsCommitteeSelectionBlock(chain, header) { if err := payoutUndelegations(chain, header, state); err != nil { return nil, nil, err } @@ -312,6 +310,14 @@ func payoutUndelegations( return nil } +// IsCommitteeSelectionBlock checks if the given header is for the committee selection block +// which can only occur on beacon chain and if epoch > pre-staking epoch. +func IsCommitteeSelectionBlock(chain engine.ChainReader, header *block.Header) bool { + isBeaconChain := header.ShardID() == shard.BeaconChainShardID + inPreStakingEra := chain.Config().IsPreStaking(header.Epoch()) + return isBeaconChain && header.IsLastBlockInEpoch() && inPreStakingEra +} + func setLastEpochInCommittee(header *block.Header, state *state.DB) error { newShardState, err := header.GetShardState() if err != nil { diff --git a/node/node_explorer.go b/node/node_explorer.go index c0c42b2ed..8f4eeffd9 100644 --- a/node/node_explorer.go +++ b/node/node_explorer.go @@ -123,7 +123,7 @@ func (node *Node) explorerMessageHandler(ctx context.Context, msg *msg_pb.Messag func (node *Node) AddNewBlockForExplorer(block *types.Block) { utils.Logger().Info().Uint64("blockHeight", block.NumberU64()).Msg("[Explorer] Adding new block for explorer node") if _, err := node.Blockchain().InsertChain([]*types.Block{block}, true); err == nil { - if len(block.Header().ShardState()) > 0 { + if block.IsLastBlockInEpoch() { node.Consensus.UpdateConsensusInformation() } // Clean up the blocks to avoid OOM. diff --git a/node/node_handler.go b/node/node_handler.go index 42c8a7920..63749dcd0 100644 --- a/node/node_handler.go +++ b/node/node_handler.go @@ -396,7 +396,7 @@ func (node *Node) PostConsensusProcessing(newBlock *types.Block) error { node.BroadcastMissingCXReceipts() // Update consensus keys at last so the change of leader status doesn't mess up normal flow - if len(newBlock.Header().ShardState()) > 0 { + if newBlock.IsLastBlockInEpoch() { node.Consensus.SetMode(node.Consensus.UpdateConsensusInformation()) } if h := node.NodeConfig.WebHooks.Hooks; h != nil { diff --git a/node/worker/worker.go b/node/worker/worker.go index 30d428301..6e93f347e 100644 --- a/node/worker/worker.go +++ b/node/worker/worker.go @@ -317,7 +317,7 @@ func (w *Worker) GetNewEpoch() *big.Int { // have an epoch and it will decide the next epoch for following blocks epoch = new(big.Int).Set(shardState.Epoch) } else { - if len(parent.Header().ShardState()) > 0 && parent.NumberU64() != 0 { + if parent.IsLastBlockInEpoch() && parent.NumberU64() != 0 { // if parent has proposed a new shard state it increases by 1, except for genesis block. epoch = epoch.Add(epoch, common.Big1) } diff --git a/rosetta/common/config.go b/rosetta/common/config.go index 810363200..30058d574 100644 --- a/rosetta/common/config.go +++ b/rosetta/common/config.go @@ -17,11 +17,11 @@ const ( // Blockchain .. Blockchain = "Harmony" - // Symbol .. - Symbol = "ONE" + // NativeSymbol .. + NativeSymbol = "ONE" - // Decimals .. - Decimals = 18 + // NativePrecision in the number of decimal places + NativePrecision = 18 // CurveType .. CurveType = types.Secp256k1 @@ -40,14 +40,14 @@ var ( // IdleTimeout .. IdleTimeout = 120 * time.Second - // Currency .. - Currency = types.Currency{ - Symbol: Symbol, - Decimals: Decimals, + // NativeCurrency .. + NativeCurrency = types.Currency{ + Symbol: NativeSymbol, + Decimals: NativePrecision, } - // CurrencyHash for quick equivalent checks - CurrencyHash = types.Hash(Currency) + // NativeCurrencyHash for quick equivalent checks + NativeCurrencyHash = types.Hash(NativeCurrency) ) // SyncStatus .. diff --git a/rosetta/common/operations.go b/rosetta/common/operations.go index 6336384f6..42eff80bf 100644 --- a/rosetta/common/operations.go +++ b/rosetta/common/operations.go @@ -10,27 +10,30 @@ import ( staking "github.com/harmony-one/harmony/staking/types" ) -// Invariant: A transaction can only contain 1 type of operation(s) other than gas expenditure. +// Invariant: A transaction can only contain 1 type of native operation(s) other than gas expenditure. const ( - // ExpendGasOperation .. + // ExpendGasOperation is an operation that only affects the native currency. ExpendGasOperation = "Gas" - // TransferOperation .. - TransferOperation = "Transfer" + // TransferNativeOperation is an operation that only affects the native currency. + TransferNativeOperation = "NativeTransfer" - // CrossShardTransferOperation .. - CrossShardTransferOperation = "CrossShardTransfer" + // CrossShardTransferNativeOperation is an operation that only affects the native currency. + CrossShardTransferNativeOperation = "NativeCrossShardTransfer" - // ContractCreationOperation .. + // ContractCreationOperation is an operation that only affects the native currency. ContractCreationOperation = "ContractCreation" - // GenesisFundsOperation .. + // GenesisFundsOperation is a special operation for genesis block only. + // Note that no transaction can be constructed with this operation. GenesisFundsOperation = "Genesis" - // PreStakingBlockRewardOperation .. + // PreStakingBlockRewardOperation is a special operation for pre-staking era only. + // Note that no transaction can be constructed with this operation. PreStakingBlockRewardOperation = "PreStakingBlockReward" - // UndelegationPayoutOperation .. + // UndelegationPayoutOperation is a special operation for committee election block only. + // Note that no transaction can be constructed with this operation. UndelegationPayoutOperation = "UndelegationPayout" ) @@ -38,8 +41,8 @@ var ( // PlainOperationTypes .. PlainOperationTypes = []string{ ExpendGasOperation, - TransferOperation, - CrossShardTransferOperation, + TransferNativeOperation, + CrossShardTransferNativeOperation, ContractCreationOperation, GenesisFundsOperation, PreStakingBlockRewardOperation, diff --git a/rosetta/common/operations_test.go b/rosetta/common/operations_test.go index a5d4465da..ac297c68a 100644 --- a/rosetta/common/operations_test.go +++ b/rosetta/common/operations_test.go @@ -50,8 +50,8 @@ func TestPlainOperationTypes(t *testing.T) { plainOperationTypes := PlainOperationTypes referenceOperationTypes := []string{ ExpendGasOperation, - TransferOperation, - CrossShardTransferOperation, + TransferNativeOperation, + CrossShardTransferNativeOperation, ContractCreationOperation, GenesisFundsOperation, PreStakingBlockRewardOperation, diff --git a/rosetta/services/account.go b/rosetta/services/account.go index 0af65cd4f..f52626195 100644 --- a/rosetta/services/account.go +++ b/rosetta/services/account.go @@ -2,6 +2,7 @@ package services import ( "context" + "fmt" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" @@ -10,6 +11,7 @@ import ( hmyTypes "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/hmy" + internalCommon "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/rosetta/common" ) @@ -25,7 +27,7 @@ func NewAccountAPI(hmy *hmy.Harmony) server.AccountAPIServicer { } } -// AccountBalance ... +// AccountBalance implements the /account/balance endpoint func (s *AccountAPI) AccountBalance( ctx context.Context, request *types.AccountBalanceRequest, ) (*types.AccountBalanceResponse, *types.Error) { @@ -73,7 +75,7 @@ func (s *AccountAPI) AccountBalance( amount := types.Amount{ Value: balance.String(), - Currency: &common.Currency, + Currency: &common.NativeCurrency, } respBlock := types.BlockIdentifier{ @@ -86,3 +88,41 @@ func (s *AccountAPI) AccountBalance( Balances: []*types.Amount{&amount}, }, nil } + +// AccountMetadata used for account identifiers +type AccountMetadata struct { + Address string `json:"hex_address"` +} + +// newAccountIdentifier .. +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(), + }) + } + metadata, err := types.MarshalMap(AccountMetadata{Address: address.String()}) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + + return &types.AccountIdentifier{ + Address: b32Address, + Metadata: metadata, + }, nil +} + +// getAddress .. +func getAddress( + identifier *types.AccountIdentifier, +) (ethCommon.Address, error) { + if identifier == nil { + return ethCommon.Address{}, fmt.Errorf("identifier cannot be nil") + } + return internalCommon.Bech32ToAddress(identifier.Address) +} diff --git a/rosetta/services/account_test.go b/rosetta/services/account_test.go new file mode 100644 index 000000000..d5c4715a5 --- /dev/null +++ b/rosetta/services/account_test.go @@ -0,0 +1,72 @@ +package services + +import ( + "reflect" + "testing" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + internalCommon "github.com/harmony-one/harmony/internal/common" +) + +func TestNewAccountIdentifier(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + addr := crypto.PubkeyToAddress(key.PublicKey) + b32Addr, err := internalCommon.AddressToBech32(addr) + if err != nil { + t.Fatalf(err.Error()) + } + metadata, err := types.MarshalMap(AccountMetadata{Address: addr.String()}) + if err != nil { + t.Fatalf(err.Error()) + } + + referenceAccID := &types.AccountIdentifier{ + Address: b32Addr, + Metadata: metadata, + } + testAccID, rosettaError := newAccountIdentifier(addr) + if rosettaError != nil { + t.Fatalf("unexpected rosetta error: %v", rosettaError) + } + if !reflect.DeepEqual(referenceAccID, testAccID) { + t.Errorf("reference ID %v != testID %v", referenceAccID, testAccID) + } +} + +func TestGetAddress(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + addr := crypto.PubkeyToAddress(key.PublicKey) + b32Addr, err := internalCommon.AddressToBech32(addr) + if err != nil { + t.Fatalf(err.Error()) + } + testAccID := &types.AccountIdentifier{ + Address: b32Addr, + } + + testAddr, err := getAddress(testAccID) + if err != nil { + t.Fatal(err) + } + if testAddr != addr { + t.Errorf("expected %v to be %v", testAddr.String(), addr.String()) + } + + defaultAddr := ethcommon.Address{} + testAddr, err = getAddress(nil) + if err == nil { + t.Error("expected err for nil identifier") + } + if testAddr != defaultAddr { + t.Errorf("expected errored addres to be %v not %v", defaultAddr.String(), testAddr.String()) + } +} diff --git a/rosetta/services/block.go b/rosetta/services/block.go index 2374fa047..a3c0b7785 100644 --- a/rosetta/services/block.go +++ b/rosetta/services/block.go @@ -2,31 +2,17 @@ package services import ( "context" - "encoding/hex" - "encoding/json" - "fmt" "math/big" - "strings" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core/rawdb" 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" - shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" - "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/rosetta/common" "github.com/harmony-one/harmony/rpc" - rpcV2 "github.com/harmony-one/harmony/rpc/v2" - "github.com/harmony-one/harmony/shard" - "github.com/harmony-one/harmony/staking" - stakingNetwork "github.com/harmony-one/harmony/staking/network" stakingTypes "github.com/harmony-one/harmony/staking/types" ) @@ -42,6 +28,11 @@ func NewBlockAPI(hmy *hmy.Harmony) server.BlockAPIServicer { } } +// BlockMetadata .. +type BlockMetadata struct { + Epoch *big.Int `json:"epoch"` +} + // Block implements the /block endpoint func (s *BlockAPI) Block( ctx context.Context, request *types.BlockRequest, @@ -83,11 +74,20 @@ func (s *BlockAPI) Block( return nil, rosettaError } + metadata, err := types.MarshalMap(BlockMetadata{ + Epoch: blk.Epoch(), + }) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } responseBlock := &types.Block{ BlockIdentifier: currBlockID, ParentBlockIdentifier: prevBlockID, Timestamp: blk.Time().Int64() * 1e3, // Timestamp must be in ms. Transactions: transactions, + Metadata: metadata, } otherTransactions := []*types.TransactionIdentifier{} @@ -103,7 +103,6 @@ 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(), @@ -112,7 +111,7 @@ func (s *BlockAPI) Block( } // Report pre-staking era block rewards as transactions to fit API. if !s.hmy.IsStakingEpoch(blk.Epoch()) { - preStakingRewardTxIDs, rosettaError := s.getAllPreStakingRewardTransactionIdentifiers(ctx, blk) + preStakingRewardTxIDs, rosettaError := s.getPreStakingRewardTransactionIdentifiers(ctx, blk) if rosettaError != nil { return nil, rosettaError } @@ -125,140 +124,28 @@ func (s *BlockAPI) Block( }, nil } -// genesisBlock is a special handler for the genesis block. -func (s *BlockAPI) genesisBlock( - ctx context.Context, request *types.BlockRequest, blk *hmytypes.Block, -) (response *types.BlockResponse, rosettaError *types.Error) { - var currBlockID, prevBlockID *types.BlockIdentifier - currBlockID = &types.BlockIdentifier{ - Index: blk.Number().Int64(), - Hash: blk.Hash().String(), - } - prevBlockID = currBlockID - - 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. - } - - otherTransactions := []*types.TransactionIdentifier{} - // Report initial genesis funds as transactions to fit API. - for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(blk.ShardID())) { - if tx.To() == nil { - return nil, common.NewError(common.CatchAllError, nil) - } - otherTransactions = append( - otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), *tx.To(), SpecialGenesisTxID), - ) - } - - return &types.BlockResponse{ - Block: responseBlock, - OtherTransactions: otherTransactions, - }, 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, - }, - }, - }, - }) +// getBlock .. +func (s *BlockAPI) getBlock( + ctx context.Context, request *types.PartialBlockIdentifier, +) (blk *hmytypes.Block, rosettaError *types.Error) { + var err error + if request.Hash != nil { + requestBlockHash := ethcommon.HexToHash(*request.Hash) + blk, err = s.hmy.GetBlock(ctx, requestBlockHash) + } else if request.Index != nil { + blk, err = s.hmy.BlockByNumber(ctx, rpc.BlockNumber(*request.Index).EthBlockNumber()) + } else { + return nil, &common.BlockNotFoundError } - 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{}{ + return nil, common.NewError(common.BlockNotFoundError, 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++ - } + if blk == nil { + return nil, &common.BlockNotFoundError } - return &blockSignerInfo{ - signers: sigInfos, - totalKeysSigned: totalSigners, - mask: mask, - blockHash: blk.Hash(), - }, nil + return blk, nil } // BlockTransaction implements the /block/transaction endpoint @@ -271,7 +158,7 @@ func (s *BlockAPI) BlockTransaction( // Format genesis block transaction request if request.BlockIdentifier.Index == 0 { - return s.genesisBlockTransaction(ctx, request) + return s.specialGenesisBlockTransaction(ctx, request) } blockHash := ethcommon.HexToHash(request.BlockIdentifier.Hash) @@ -290,12 +177,12 @@ func (s *BlockAPI) BlockTransaction( var transaction *types.Transaction if txInfo.tx != nil && txInfo.receipt != nil { - transaction, rosettaError = formatTransaction(txInfo.tx, txInfo.receipt) + transaction, rosettaError = FormatTransaction(txInfo.tx, txInfo.receipt) if rosettaError != nil { return nil, rosettaError } } else if txInfo.cxReceipt != nil { - transaction, rosettaError = formatCrossShardReceiverTransaction(txInfo.cxReceipt) + transaction, rosettaError = FormatCrossShardReceiverTransaction(txInfo.cxReceipt) if rosettaError != nil { return nil, rosettaError } @@ -305,136 +192,6 @@ func (s *BlockAPI) BlockTransaction( return &types.BlockTransactionResponse{Transaction: transaction}, nil } -// genesisBlockTransaction is a special handler for genesis block transactions -func (s *BlockAPI) genesisBlockTransaction( - ctx context.Context, request *types.BlockTransactionRequest, -) (response *types.BlockTransactionResponse, rosettaError *types.Error) { - genesisBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(0).EthBlockNumber()) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - 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, address, s.hmy.ShardID) - if rosettaError != nil { - return nil, rosettaError - } - return &types.BlockTransactionResponse{Transaction: txs}, nil -} - -// 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, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialPreStakingRewardTxID) - 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(), - ), - }) - } - blockSignerInfo, rosettaError := s.getBlockSignerInfo(ctx, blk) - if rosettaError != nil { - return nil, rosettaError - } - 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 - } - return &types.BlockTransactionResponse{Transaction: transactions}, nil -} - -// getBlock .. -func (s *BlockAPI) getBlock( - ctx context.Context, request *types.PartialBlockIdentifier, -) (blk *hmytypes.Block, rosettaError *types.Error) { - var err error - if request.Hash != nil { - requestBlockHash := ethcommon.HexToHash(*request.Hash) - blk, err = s.hmy.GetBlock(ctx, requestBlockHash) - } else if request.Index != nil { - blk, err = s.hmy.BlockByNumber(ctx, rpc.BlockNumber(*request.Index).EthBlockNumber()) - } else { - return nil, &common.BlockNotFoundError - } - if err != nil { - return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{ - "message": err.Error(), - }) - } - if blk == nil { - return nil, &common.BlockNotFoundError - } - return blk, nil -} - // transactionInfo stores all related information for any transaction on the Harmony chain // Note that some elements can be nil if not applicable type transactionInfo struct { @@ -488,808 +245,3 @@ func (s *BlockAPI) getTransactionInfo( cxReceipt: cxReceipt, }, nil } - -func (s *BlockAPI) getTransactionReceiptFromIndex( - ctx context.Context, blockHash ethcommon.Hash, index uint64, -) (*hmytypes.Receipt, *types.Error) { - receipts, err := s.hmy.GetReceipts(ctx, blockHash) - if err != nil || len(receipts) <= int(index) { - message := fmt.Sprintf("Transasction receipt not found") - if err != nil { - message = fmt.Sprintf("Transasction receipt not found: %v", err.Error()) - } - return nil, common.NewError(common.ReceiptNotFoundError, map[string]interface{}{ - "message": message, - }) - } - return receipts[index], nil -} - -// getPseudoTransactionForGenesis to create unsigned transaction that contain genesis funds. -// Note that this is for internal usage only. Genesis funds are not transactions. -func getPseudoTransactionForGenesis(spec *core.Genesis) []*hmytypes.Transaction { - txs := []*hmytypes.Transaction{} - for acc, bal := range spec.Alloc { - txs = append(txs, hmytypes.NewTransaction( - 0, acc, spec.ShardID, bal.Balance, 0, big.NewInt(0), spec.ExtraData, - )) - } - 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, and undelegation payouts. -// Must include block hash to guarantee uniqueness of tx identifiers. -func getSpecialCaseTransactionIdentifier( - blockHash ethcommon.Hash, address ethcommon.Address, suffix SpecialTransactionSuffix, -) *types.TransactionIdentifier { - return &types.TransactionIdentifier{ - 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, expectedSuffix SpecialTransactionSuffix, -) (ethcommon.Hash, ethcommon.Address, *types.Error) { - hash := txID.Hash - hash = strings.TrimPrefix(hash, "0x") - hash = strings.TrimPrefix(hash, "0X") - 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", - }) - } - 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 -type blockSignerInfo struct { - // signers is a map of addresses in the signers for the block to - // all of the serialized BLS keys that signed said block. - signers map[ethcommon.Address][]bls.SerializedPublicKey - // totalKeysSigned is the total number of bls keys that signed the block. - totalKeysSigned uint - // mask is the bitmap mask for the block. - mask *bls.Mask - blockHash ethcommon.Hash -} - -// TransactionMetadata .. -type TransactionMetadata struct { - // CrossShardIdentifier is the transaction identifier on the from/source shard - CrossShardIdentifier *types.TransactionIdentifier `json:"cross_shard_transaction_identifier,omitempty"` - ToShardID *uint32 `json:"to_shard,omitempty"` - FromShardID *uint32 `json:"from_shard,omitempty"` - Data *string `json:"data,omitempty"` - Logs []*hmytypes.Log `json:"logs,omitempty"` -} - -// UnmarshalFromInterface .. -func (t *TransactionMetadata) UnmarshalFromInterface(metaData interface{}) error { - var args TransactionMetadata - dat, err := json.Marshal(metaData) - if err != nil { - return err - } - if err := json.Unmarshal(dat, &args); err != nil { - return err - } - *t = args - return nil -} - -// formatCrossShardReceiverTransaction for cross-shard payouts on destination shard -func formatCrossShardReceiverTransaction( - cxReceipt *hmytypes.CXReceipt, -) (txs *types.Transaction, rosettaError *types.Error) { - ctxID := &types.TransactionIdentifier{Hash: cxReceipt.TxHash.String()} - senderAccountID, rosettaError := newAccountIdentifier(cxReceipt.From) - if rosettaError != nil { - return nil, rosettaError - } - receiverAccountID, rosettaError := newAccountIdentifier(*cxReceipt.To) - if rosettaError != nil { - return nil, rosettaError - } - metadata, err := types.MarshalMap(TransactionMetadata{ - CrossShardIdentifier: ctxID, - ToShardID: &cxReceipt.ToShardID, - FromShardID: &cxReceipt.ShardID, - }) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - opMetadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ - From: senderAccountID, - To: receiverAccountID, - }) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - - return &types.Transaction{ - TransactionIdentifier: ctxID, - Metadata: metadata, - Operations: []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: 0, // There is no gas expenditure for cross-shard transaction payout - }, - Type: common.CrossShardTransferOperation, - Status: common.SuccessOperationStatus.Status, - Account: receiverAccountID, - Amount: &types.Amount{ - Value: fmt.Sprintf("%v", cxReceipt.Amount), - Currency: &common.Currency, - }, - Metadata: opMetadata, - }, - }, - }, nil -} - -// formatGenesisTransaction for genesis block's initial balances -func formatGenesisTransaction( - txID *types.TransactionIdentifier, targetAddr ethcommon.Address, shardID uint32, -) (fmtTx *types.Transaction, rosettaError *types.Error) { - var b32Addr string - targetB32Addr := internalCommon.MustAddressToBech32(targetAddr) - for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(shardID)) { - if tx.To() == nil { - return nil, common.NewError(common.CatchAllError, nil) - } - b32Addr = internalCommon.MustAddressToBech32(*tx.To()) - if targetB32Addr == b32Addr { - accID, rosettaError := newAccountIdentifier(*tx.To()) - if rosettaError != nil { - return nil, rosettaError - } - return &types.Transaction{ - TransactionIdentifier: txID, - Operations: []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: 0, - }, - Type: common.GenesisFundsOperation, - Status: common.SuccessOperationStatus.Status, - Account: accID, - Amount: &types.Amount{ - Value: fmt.Sprintf("%v", tx.Value()), - Currency: &common.Currency, - }, - }, - }, - }, nil - } - } - return nil, &common.TransactionNotFoundError -} - -// 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) { - signatures, ok := blockSigInfo.signers[address] - if !ok || len(signatures) == 0 { - return nil, &common.TransactionNotFoundError - } - accID, rosettaError := newAccountIdentifier(address) - if rosettaError != nil { - return nil, rosettaError - } - - // Calculate rewards exactly like `AccumulateRewardsAndCountSigs` but short circuit when possible. - var rewardsForThisBlock *big.Int - i := 0 - last := big.NewInt(0) - count := big.NewInt(int64(blockSigInfo.totalKeysSigned)) - for sigAddr, keys := range blockSigInfo.signers { - rewardsForThisAddr := big.NewInt(0) - for range keys { - cur := big.NewInt(0) - cur.Mul(stakingNetwork.BlockReward, big.NewInt(int64(i+1))).Div(cur, count) - reward := big.NewInt(0).Sub(cur, last) - rewardsForThisAddr = new(big.Int).Add(reward, rewardsForThisAddr) - last = cur - i++ - } - if 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 era for block signer", - }) - } - break - } - } - - return &types.Transaction{ - TransactionIdentifier: txID, - Operations: []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: 0, - }, - Type: common.PreStakingBlockRewardOperation, - Status: common.SuccessOperationStatus.Status, - Account: accID, - Amount: &types.Amount{ - Value: fmt.Sprintf("%v", rewardsForThisBlock), - Currency: &common.Currency, - }, - }, - }, - }, 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 - -} - -var ( - // DefaultSenderAddress .. - DefaultSenderAddress = ethcommon.HexToAddress("0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE") -) - -// formatTransaction for staking, cross-shard sender, and plain transactions -func formatTransaction( - tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt, -) (fmtTx *types.Transaction, rosettaError *types.Error) { - var operations []*types.Operation - var isCrossShard, isStaking bool - var toShard uint32 - - switch tx.(type) { - case *stakingTypes.StakingTransaction: - isStaking = true - stakingTx := tx.(*stakingTypes.StakingTransaction) - operations, rosettaError = getStakingOperations(stakingTx, receipt) - if rosettaError != nil { - return nil, rosettaError - } - isCrossShard = false - toShard = stakingTx.ShardID() - case *hmytypes.Transaction: - isStaking = false - plainTx := tx.(*hmytypes.Transaction) - operations, rosettaError = getOperations(plainTx, receipt) - if rosettaError != nil { - return nil, rosettaError - } - isCrossShard = plainTx.ShardID() != plainTx.ToShardID() - toShard = plainTx.ToShardID() - default: - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": "unknown transaction type", - }) - } - fromShard := tx.ShardID() - txID := &types.TransactionIdentifier{Hash: tx.Hash().String()} - - // Set all possible metadata - var txMetadata TransactionMetadata - if isCrossShard { - txMetadata.CrossShardIdentifier = txID - txMetadata.ToShardID = &toShard - txMetadata.FromShardID = &fromShard - } - if len(tx.Data()) > 0 && !isStaking { - hexData := hex.EncodeToString(tx.Data()) - txMetadata.Data = &hexData - txMetadata.Logs = receipt.Logs - } - metadata, err := types.MarshalMap(txMetadata) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - - return &types.Transaction{ - TransactionIdentifier: txID, - Operations: operations, - Metadata: metadata, - }, nil -} - -// getOperations for one of the following transactions: -// contract creation, cross-shard sender, same-shard transfer -func getOperations( - tx *hmytypes.Transaction, receipt *hmytypes.Receipt, -) ([]*types.Operation, *types.Error) { - senderAddress, err := tx.SenderAddress() - if err != nil { - senderAddress = DefaultSenderAddress - } - accountID, rosettaError := newAccountIdentifier(senderAddress) - if rosettaError != nil { - return nil, rosettaError - } - - // All operations excepts for cross-shard tx payout expend gas - gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice()) - gasOperations := newOperations(gasExpended, accountID) - - // Handle different cases of plain transactions - var txOperations []*types.Operation - if tx.To() == nil { - txOperations, rosettaError = newContractCreationOperations( - gasOperations[0].OperationIdentifier, tx, receipt, senderAddress, - ) - } else if tx.ShardID() != tx.ToShardID() { - txOperations, rosettaError = newCrossShardSenderTransferOperations( - gasOperations[0].OperationIdentifier, tx, senderAddress, - ) - } else { - txOperations, rosettaError = newTransferOperations( - gasOperations[0].OperationIdentifier, tx, receipt, senderAddress, - ) - } - if rosettaError != nil { - return nil, rosettaError - } - - return append(gasOperations, txOperations...), nil -} - -// getStakingOperations for all staking directives -func getStakingOperations( - tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt, -) ([]*types.Operation, *types.Error) { - senderAddress, err := tx.SenderAddress() - if err != nil { - senderAddress = DefaultSenderAddress - } - accountID, rosettaError := newAccountIdentifier(senderAddress) - if rosettaError != nil { - return nil, rosettaError - } - - // All operations excepts for cross-shard tx payout expend gas - gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice()) - gasOperations := newOperations(gasExpended, accountID) - - // Format staking message for metadata using decimal numbers (hence usage of rpcV2) - rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - metadata, err := types.MarshalMap(rpcStakingTx.Msg) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - - // Set correct amount depending on staking message directive that apply balance changes INSTANTLY - var amount *types.Amount - switch tx.StakingType() { - case stakingTypes.DirectiveCreateValidator: - if amount, rosettaError = getAmountFromCreateValidatorMessage(tx.Data()); rosettaError != nil { - return nil, rosettaError - } - case stakingTypes.DirectiveDelegate: - if amount, rosettaError = getAmountFromDelegateMessage(receipt, tx.Data()); rosettaError != nil { - return nil, rosettaError - } - case stakingTypes.DirectiveCollectRewards: - if amount, rosettaError = getAmountFromCollectRewards(receipt, senderAddress); rosettaError != nil { - return nil, rosettaError - } - default: - amount = &types.Amount{ - Value: "0", // All other staking transactions do not apply balance changes instantly or at all - Currency: &common.Currency, - } - } - - return append(gasOperations, &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: gasOperations[0].OperationIdentifier.Index + 1, - }, - RelatedOperations: []*types.OperationIdentifier{ - gasOperations[0].OperationIdentifier, - }, - Type: tx.StakingType().String(), - Status: common.SuccessOperationStatus.Status, - Account: accountID, - Amount: amount, - Metadata: metadata, - }), nil -} - -func getAmountFromCreateValidatorMessage(data []byte) (*types.Amount, *types.Error) { - msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveCreateValidator) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - stkMsg, ok := msg.(*stakingTypes.CreateValidator) - if !ok { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": "unable to parse staking message for create validator tx", - }) - } - return &types.Amount{ - Value: formatNegativeValue(stkMsg.Amount), - Currency: &common.Currency, - }, nil -} - -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{}{ - "message": err.Error(), - }) - } - stkMsg, ok := msg.(*stakingTypes.Delegate) - if !ok { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "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: formatNegativeValue(stkAmount), - Currency: &common.Currency, - }, nil -} - -func getAmountFromCollectRewards( - receipt *hmytypes.Receipt, senderAddress ethcommon.Address, -) (*types.Amount, *types.Error) { - var amount *types.Amount - logs := findLogsWithTopic(receipt, staking.CollectRewardsTopic) - for _, log := range logs { - if log.Address == senderAddress { - amount = &types.Amount{ - Value: fmt.Sprintf("%v", big.NewInt(0).SetBytes(log.Data)), - Currency: &common.Currency, - } - break - } - } - if amount == nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": fmt.Sprintf("collect rewards amount not found for %v", senderAddress), - }) - } - return amount, nil -} - -// newTransferOperations extracts & formats the operation(s) for plain transaction, -// including contract transactions. -func newTransferOperations( - startingOperationID *types.OperationIdentifier, - tx *hmytypes.Transaction, receipt *hmytypes.Receipt, senderAddress ethcommon.Address, -) ([]*types.Operation, *types.Error) { - if tx.To() == nil { - return nil, common.NewError(common.CatchAllError, nil) - } - receiverAddress := *tx.To() - - // Common elements - opType := common.TransferOperation - opStatus := common.SuccessOperationStatus.Status - if receipt.Status == hmytypes.ReceiptStatusFailed { - if len(tx.Data()) > 0 { - opStatus = common.ContractFailureOperationStatus.Status - } else { - // Should never see a failed non-contract related transaction on chain - opStatus = common.FailureOperationStatus.Status - utils.Logger().Warn().Msgf("Failed transaction on chain: %v", tx.Hash().String()) - } - } - - // Subtraction operation elements - subOperationID := &types.OperationIdentifier{ - Index: startingOperationID.Index + 1, - } - subRelatedID := []*types.OperationIdentifier{ - startingOperationID, - } - subAccountID, rosettaError := newAccountIdentifier(senderAddress) - if rosettaError != nil { - return nil, rosettaError - } - subAmount := &types.Amount{ - Value: formatNegativeValue(tx.Value()), - Currency: &common.Currency, - } - - // Addition operation elements - addOperationID := &types.OperationIdentifier{ - Index: subOperationID.Index + 1, - } - addRelatedID := []*types.OperationIdentifier{ - subOperationID, - } - addAccountID, rosettaError := newAccountIdentifier(receiverAddress) - if rosettaError != nil { - return nil, rosettaError - } - addAmount := &types.Amount{ - Value: fmt.Sprintf("%v", tx.Value()), - Currency: &common.Currency, - } - - return []*types.Operation{ - { - OperationIdentifier: subOperationID, - RelatedOperations: subRelatedID, - Type: opType, - Status: opStatus, - Account: subAccountID, - Amount: subAmount, - }, - { - OperationIdentifier: addOperationID, - RelatedOperations: addRelatedID, - Type: opType, - Status: opStatus, - Account: addAccountID, - Amount: addAmount, - }, - }, nil -} - -// newCrossShardSenderTransferOperations extracts & formats the operation(s) for cross-shard-tx -// on the sender's shard. -func newCrossShardSenderTransferOperations( - startingOperationID *types.OperationIdentifier, - tx *hmytypes.Transaction, senderAddress ethcommon.Address, -) ([]*types.Operation, *types.Error) { - if tx.To() == nil { - return nil, common.NewError(common.CatchAllError, nil) - } - senderAccountID, rosettaError := newAccountIdentifier(senderAddress) - if rosettaError != nil { - return nil, rosettaError - } - receiverAccountID, rosettaError := newAccountIdentifier(*tx.To()) - if rosettaError != nil { - return nil, rosettaError - } - metadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ - From: senderAccountID, - To: receiverAccountID, - }) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - - return []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: startingOperationID.Index + 1, - }, - RelatedOperations: []*types.OperationIdentifier{ - startingOperationID, - }, - Type: common.CrossShardTransferOperation, - Status: common.SuccessOperationStatus.Status, - Account: senderAccountID, - Amount: &types.Amount{ - Value: formatNegativeValue(tx.Value()), - Currency: &common.Currency, - }, - Metadata: metadata, - }, - }, nil -} - -// newContractCreationOperations extracts & formats the operation(s) for a contract creation tx -func newContractCreationOperations( - startingOperationID *types.OperationIdentifier, - tx *hmytypes.Transaction, txReceipt *hmytypes.Receipt, senderAddress ethcommon.Address, -) ([]*types.Operation, *types.Error) { - senderAccountID, rosettaError := newAccountIdentifier(senderAddress) - if rosettaError != nil { - return nil, rosettaError - } - - // Set execution status as necessary - status := common.SuccessOperationStatus.Status - if txReceipt.Status == hmytypes.ReceiptStatusFailed { - status = common.ContractFailureOperationStatus.Status - } - contractAddressID, rosettaError := newAccountIdentifier(txReceipt.ContractAddress) - if rosettaError != nil { - return nil, rosettaError - } - - return []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: startingOperationID.Index + 1, - }, - RelatedOperations: []*types.OperationIdentifier{ - startingOperationID, - }, - Type: common.ContractCreationOperation, - Status: status, - Account: senderAccountID, - Amount: &types.Amount{ - Value: formatNegativeValue(tx.Value()), - Currency: &common.Currency, - }, - Metadata: map[string]interface{}{ - "contract_address": contractAddressID, - }, - }, - }, nil -} - -// AccountMetadata used for account identifiers -type AccountMetadata struct { - Address string `json:"hex_address"` -} - -// newAccountIdentifier .. -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(), - }) - } - metadata, err := types.MarshalMap(AccountMetadata{Address: address.String()}) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - - return &types.AccountIdentifier{ - Address: b32Address, - Metadata: metadata, - }, nil -} - -// getAddress .. -func getAddress( - identifier *types.AccountIdentifier, -) (ethcommon.Address, error) { - if identifier == nil { - return ethcommon.Address{}, fmt.Errorf("identifier cannot be nil") - } - return internalCommon.Bech32ToAddress(identifier.Address) -} - -// newOperations creates a new operation with the gas fee as the first operation. -// Note: the gas fee is gasPrice * gasUsed. -func newOperations( - gasFeeInATTO *big.Int, accountID *types.AccountIdentifier, -) []*types.Operation { - return []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: 0, // gas operation is always first - }, - Type: common.ExpendGasOperation, - Status: common.SuccessOperationStatus.Status, - Account: accountID, - Amount: &types.Amount{ - Value: formatNegativeValue(gasFeeInATTO), - Currency: &common.Currency, - }, - }, - } -} - -// findLogsWithTopic returns all the logs that contain the given receipt -func findLogsWithTopic( - receipt *hmytypes.Receipt, targetTopic ethcommon.Hash, -) []*hmytypes.Log { - logs := []*hmytypes.Log{} - for _, log := range receipt.Logs { - for _, topic := range log.Topics { - if topic == targetTopic { - logs = append(logs, log) - break - } - } - } - return logs -} - -// formatNegativeValue .. -func formatNegativeValue(num *big.Int) string { - value := "0" - if num != nil && num.Cmp(big.NewInt(0)) == 1 { - value = fmt.Sprintf("-%v", new(big.Int).Abs(num)) - } - return value -} - -// getGenesisSpec .. -func getGenesisSpec(shardID uint32) *core.Genesis { - if shard.Schedule.GetNetworkID() == shardingconfig.MainNet { - return core.NewGenesisSpec(nodeconfig.Mainnet, shardID) - } - if shard.Schedule.GetNetworkID() == shardingconfig.LocalNet { - return core.NewGenesisSpec(nodeconfig.Localnet, shardID) - } - return core.NewGenesisSpec(nodeconfig.Testnet, shardID) -} diff --git a/rosetta/services/block_special.go b/rosetta/services/block_special.go new file mode 100644 index 000000000..882590b1c --- /dev/null +++ b/rosetta/services/block_special.go @@ -0,0 +1,334 @@ +package services + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/harmony-one/harmony/core" + hmytypes "github.com/harmony-one/harmony/core/types" + internalCommon "github.com/harmony-one/harmony/internal/common" + nodeconfig "github.com/harmony-one/harmony/internal/configs/node" + shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" + "github.com/harmony-one/harmony/rosetta/common" + "github.com/harmony-one/harmony/rpc" + "github.com/harmony-one/harmony/shard" +) + +// SpecialTransactionSuffix enum for all special transactions +type SpecialTransactionSuffix uint + +// Special transaction suffixes that are specific to the rosetta package +const ( + SpecialGenesisTxID SpecialTransactionSuffix = iota + SpecialPreStakingRewardTxID + SpecialUndelegationPayoutTxID +) + +// Length for special case transaction identifiers +const ( + blockHashStrLen = 64 + bech32AddrStrLen = 42 +) + +// 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, and undelegation payouts. +// Must include block hash to guarantee uniqueness of tx identifiers. +func getSpecialCaseTransactionIdentifier( + blockHash ethcommon.Hash, address ethcommon.Address, suffix SpecialTransactionSuffix, +) *types.TransactionIdentifier { + return &types.TransactionIdentifier{ + Hash: fmt.Sprintf("%v_%v_%v", + blockHash.String(), internalCommon.MustAddressToBech32(address), suffix.String(), + ), + } +} + +// unpackSpecialCaseTransactionIdentifier returns the suffix & blockHash if the txID is formatted correctly. +func unpackSpecialCaseTransactionIdentifier( + txID *types.TransactionIdentifier, expectedSuffix SpecialTransactionSuffix, +) (ethcommon.Hash, ethcommon.Address, *types.Error) { + hash := txID.Hash + hash = strings.TrimPrefix(hash, "0x") + hash = strings.TrimPrefix(hash, "0X") + minCharCount := blockHashStrLen + bech32AddrStrLen + 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", + }) + } + blkHash := ethcommon.HexToHash(hash[:blockHashStrLen]) + addr := internalCommon.MustBech32ToAddress(hash[blockHashStrLen+1 : minCharCount-1]) + return blkHash, addr, nil +} + +// genesisBlock is a special handler for the genesis block. +func (s *BlockAPI) genesisBlock( + ctx context.Context, request *types.BlockRequest, blk *hmytypes.Block, +) (response *types.BlockResponse, rosettaError *types.Error) { + var currBlockID, prevBlockID *types.BlockIdentifier + currBlockID = &types.BlockIdentifier{ + Index: blk.Number().Int64(), + Hash: blk.Hash().String(), + } + prevBlockID = currBlockID + + metadata, err := types.MarshalMap(BlockMetadata{ + Epoch: blk.Epoch(), + }) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + 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. + Metadata: metadata, + } + + otherTransactions := []*types.TransactionIdentifier{} + // Report initial genesis funds as transactions to fit API. + for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(blk.ShardID())) { + if tx.To() == nil { + return nil, common.NewError(common.CatchAllError, nil) + } + otherTransactions = append( + otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), *tx.To(), SpecialGenesisTxID), + ) + } + + return &types.BlockResponse{ + Block: responseBlock, + OtherTransactions: otherTransactions, + }, nil +} + +// getPseudoTransactionForGenesis to create unsigned transaction that contain genesis funds. +// Note that this is for internal usage only. Genesis funds are not transactions. +func getPseudoTransactionForGenesis(spec *core.Genesis) []*hmytypes.Transaction { + txs := []*hmytypes.Transaction{} + for acc, bal := range spec.Alloc { + txs = append(txs, hmytypes.NewTransaction( + 0, acc, spec.ShardID, bal.Balance, 0, big.NewInt(0), spec.ExtraData, + )) + } + return txs +} + +// specialGenesisBlockTransaction is a special handler for genesis block transactions +func (s *BlockAPI) specialGenesisBlockTransaction( + ctx context.Context, request *types.BlockTransactionRequest, +) (response *types.BlockTransactionResponse, rosettaError *types.Error) { + genesisBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(0).EthBlockNumber()) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + 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, address, s.hmy.ShardID) + if rosettaError != nil { + return nil, rosettaError + } + return &types.BlockTransactionResponse{Transaction: txs}, nil +} + +// getPreStakingRewardTransactionIdentifiers is only used for the /block endpoint +// rewards for signing block n is paid out on block n+1 +func (s *BlockAPI) getPreStakingRewardTransactionIdentifiers( + ctx context.Context, currBlock *hmytypes.Block, +) ([]*types.TransactionIdentifier, *types.Error) { + if currBlock.Number().Cmp(big.NewInt(1)) != 1 { + return nil, nil + } + blockNumToBeRewarded := currBlock.Number().Uint64() - 1 + rewardedBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blockNumToBeRewarded).EthBlockNumber()) + if err != nil { + return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{ + "message": err.Error(), + }) + } + blockSigInfo, err := s.hmy.GetDetailedBlockSignerInfo(ctx, rewardedBlock) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + txIDs := []*types.TransactionIdentifier{} + for acc, signedBlsKeys := range blockSigInfo.Signers { + if len(signedBlsKeys) > 0 { + txIDs = append(txIDs, getSpecialCaseTransactionIdentifier(currBlock.Hash(), acc, SpecialPreStakingRewardTxID)) + } + } + return txIDs, nil +} + +// 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.hmy.IsCommitteeSelectionBlock(blk.Header()) { + // 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) { + if blk.Number().Cmp(big.NewInt(1)) != 1 { + return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{ + "message": "block does not contain any pre-staking era block rewards", + }) + } + blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialPreStakingRewardTxID) + if rosettaError != nil { + return nil, rosettaError + } + blockNumOfSigsForReward := blk.Number().Uint64() - 1 + signedBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blockNumOfSigsForReward).EthBlockNumber()) + if err != nil { + return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{ + "message": err.Error(), + }) + } + if blkHash.String() != blk.Hash().String() { + return nil, common.NewError(common.SanityCheckError, map[string]interface{}{ + "message": fmt.Sprintf( + "block hash %v != requested block hash %v in tx ID", blkHash.String(), blk.Hash().String(), + ), + }) + } + blockSignerInfo, err := s.hmy.GetDetailedBlockSignerInfo(ctx, signedBlock) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + 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 + } + return &types.BlockTransactionResponse{Transaction: transactions}, nil +} + +// getAllUndelegationPayoutTransactions is only used for the /block endpoint +func (s *BlockAPI) getAllUndelegationPayoutTransactions( + ctx context.Context, blk *hmytypes.Block, +) ([]*types.Transaction, *types.Error) { + if !s.hmy.IsCommitteeSelectionBlock(blk.Header()) { + 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: payout.String(), + Currency: &common.NativeCurrency, + }, + }, + }, + }) + } + return transactions, nil +} + +// getGenesisSpec .. +func getGenesisSpec(shardID uint32) *core.Genesis { + if shard.Schedule.GetNetworkID() == shardingconfig.MainNet { + return core.NewGenesisSpec(nodeconfig.Mainnet, shardID) + } + if shard.Schedule.GetNetworkID() == shardingconfig.LocalNet { + return core.NewGenesisSpec(nodeconfig.Localnet, shardID) + } + return core.NewGenesisSpec(nodeconfig.Testnet, shardID) +} diff --git a/rosetta/services/block_special_test.go b/rosetta/services/block_special_test.go new file mode 100644 index 000000000..f813a8424 --- /dev/null +++ b/rosetta/services/block_special_test.go @@ -0,0 +1,77 @@ +package services + +import ( + "fmt" + "math/big" + "reflect" + "testing" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/harmony-one/harmony/core" + internalCommon "github.com/harmony-one/harmony/internal/common" + nodeconfig "github.com/harmony-one/harmony/internal/configs/node" + "github.com/harmony-one/harmony/rosetta/common" +) + +var ( + oneBig = big.NewInt(1e18) + tenOnes = new(big.Int).Mul(big.NewInt(10), oneBig) + twelveOnes = new(big.Int).Mul(big.NewInt(12), oneBig) + gasPrice = big.NewInt(10000) +) + +func TestGetPseudoTransactionForGenesis(t *testing.T) { + genesisSpec := core.NewGenesisSpec(nodeconfig.Testnet, 0) + txs := getPseudoTransactionForGenesis(genesisSpec) + for acc := range genesisSpec.Alloc { + found := false + for _, tx := range txs { + if acc == *tx.To() { + found = true + break + } + } + if !found { + t.Error("unable to find genesis account in generated pseudo transactions") + } + } +} + +func TestSpecialCaseTransactionIdentifier(t *testing.T) { + testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") + testB32Address := "one10g7kfque6ew2jjfxxa6agkdwk4wlyjuncp6gwz" + testAddress := internalCommon.MustBech32ToAddress(testB32Address) + refTxID := &types.TransactionIdentifier{ + Hash: fmt.Sprintf("%v_%v_%v", testBlkHash.String(), testB32Address, SpecialGenesisTxID.String()), + } + specialTxID := getSpecialCaseTransactionIdentifier( + testBlkHash, testAddress, SpecialGenesisTxID, + ) + if !reflect.DeepEqual(refTxID, specialTxID) { + t.Fatal("invalid for mate for special case TxID") + } + unpackedBlkHash, unpackedAddress, rosettaError := unpackSpecialCaseTransactionIdentifier( + specialTxID, SpecialGenesisTxID, + ) + if rosettaError != nil { + t.Fatal(rosettaError) + } + 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: ""}, SpecialGenesisTxID, + ) + if rosettaError == nil { + t.Fatal("expected rosetta error") + } + if rosettaError.Code != common.CatchAllError.Code { + t.Error("expected error code to be catch call error") + } +} diff --git a/rosetta/services/block_test.go b/rosetta/services/block_test.go deleted file mode 100644 index 789230e6c..000000000 --- a/rosetta/services/block_test.go +++ /dev/null @@ -1,1326 +0,0 @@ -package services - -import ( - "crypto/ecdsa" - "fmt" - "math/big" - "reflect" - "testing" - - "github.com/coinbase/rosetta-sdk-go/types" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - - "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" - "github.com/harmony-one/harmony/rosetta/common" - rpcV2 "github.com/harmony-one/harmony/rpc/v2" - "github.com/harmony-one/harmony/staking" - stakingNetwork "github.com/harmony-one/harmony/staking/network" - stakingTypes "github.com/harmony-one/harmony/staking/types" -) - -var ( - oneBig = big.NewInt(1e18) - tenOnes = new(big.Int).Mul(big.NewInt(10), oneBig) - twelveOnes = new(big.Int).Mul(big.NewInt(12), oneBig) - gasPrice = big.NewInt(10000) -) - -func createTestStakingTransaction( - payloadMaker func() (stakingTypes.Directive, interface{}), key *ecdsa.PrivateKey, nonce, gasLimit uint64, -) (*stakingTypes.StakingTransaction, error) { - tx, err := stakingTypes.NewStakingTransaction(nonce, gasLimit, gasPrice, payloadMaker) - if err != nil { - return nil, err - } - if key == nil { - key, err = crypto.GenerateKey() - if err != nil { - return nil, err - } - } - // Staking transactions are always post EIP155 epoch - return stakingTypes.Sign(tx, stakingTypes.NewEIP155Signer(tx.ChainID()), key) -} - -func getMessageFromStakingTx(tx *stakingTypes.StakingTransaction) (map[string]interface{}, error) { - rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0) - if err != nil { - return nil, err - } - return types.MarshalMap(rpcStakingTx.Msg) -} - -func createTestTransaction( - signer hmytypes.Signer, fromShard, toShard uint32, nonce, gasLimit uint64, amount *big.Int, data []byte, -) (*hmytypes.Transaction, error) { - fromKey, err := crypto.GenerateKey() - if err != nil { - return nil, err - } - toKey, err := crypto.GenerateKey() - if err != nil { - return nil, err - } - toAddr := crypto.PubkeyToAddress(toKey.PublicKey) - var tx *hmytypes.Transaction - if fromShard != toShard { - tx = hmytypes.NewCrossShardTransaction( - nonce, &toAddr, fromShard, toShard, amount, gasLimit, gasPrice, data, - ) - } else { - tx = hmytypes.NewTransaction( - nonce, toAddr, fromShard, amount, gasLimit, gasPrice, data, - ) - } - return hmytypes.SignTx(tx, signer, fromKey) -} - -func createTestContractCreationTransaction( - signer hmytypes.Signer, shard uint32, nonce, gasLimit uint64, data []byte, -) (*hmytypes.Transaction, error) { - fromKey, err := crypto.GenerateKey() - if err != nil { - return nil, err - } - tx := hmytypes.NewContractCreation(nonce, shard, big.NewInt(0), gasLimit, gasPrice, data) - return hmytypes.SignTx(tx, signer, fromKey) -} - -// Invariant: A transaction can only contain 1 type of operation(s) other than gas expenditure. -func assertOperationTypeUniquenessInvariant(operations []*types.Operation) error { - foundType := "" - for _, op := range operations { - if op.Type == common.ExpendGasOperation { - continue - } - if foundType == "" { - foundType = op.Type - } - if op.Type != foundType { - return fmt.Errorf("found more than 1 type in given set of operations") - } - } - return nil -} - -// Note that this test only checks the general format of each type transaction on Harmony. -// The detailed operation checks for each type of transaction is done in separate unit tests. -func TestFormatTransactionIntegration(t *testing.T) { - gasLimit := uint64(1e18) - gasUsed := uint64(1e5) - senderKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - receiverKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - - testFormatStakingTransaction(t, gasLimit, gasUsed, senderKey, receiverKey) - testFormatPlainTransaction(t, gasLimit, gasUsed, senderKey, receiverKey) - // Note that cross-shard receiver operations/transactions are formatted via - // formatCrossShardReceiverTransaction, thus, it is not tested here -- but tested on its own. - testFormatCrossShardSenderTransaction(t, gasLimit, gasUsed, senderKey, receiverKey) -} - -func testFormatStakingTransaction( - t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey, -) { - senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) - receiverAddr := crypto.PubkeyToAddress(receiverKey.PublicKey) - tx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { - return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{ - DelegatorAddress: senderAddr, - ValidatorAddress: receiverAddr, - Amount: tenOnes, - } - }, senderKey, 0, gasLimit) - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, - GasUsed: gasUsed, - } - rosettaTx, rosettaError := formatTransaction(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - if len(rosettaTx.Operations) != 2 { - t.Error("Expected 2 operations") - } - if err := assertOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { - t.Error(err) - } - if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() { - t.Error("Invalid transaction") - } - if rosettaTx.Operations[0].Type != common.ExpendGasOperation { - t.Error("Expected 1st operation to be gas type") - } - if rosettaTx.Operations[1].Type != tx.StakingType().String() { - t.Error("Expected 2nd operation to be staking type") - } - if reflect.DeepEqual(rosettaTx.Operations[1].Metadata, map[string]interface{}{}) { - t.Error("Expected staking operation to have some metadata") - } - if !reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) { - t.Error("Expected transaction to have no metadata") - } - if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) { - t.Error("Expected sender to pay gas fee") - } -} - -func testFormatPlainTransaction( - t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey, -) { - // Note that post EIP-155 epoch singer is tested in detailed tests. - signer := hmytypes.HomesteadSigner{} - tx, err := createTestTransaction( - signer, 0, 0, 0, 1e18, big.NewInt(1), []byte("test"), - ) - if err != nil { - t.Fatal(err.Error()) - } - senderAddr, err := tx.SenderAddress() - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, - GasUsed: gasUsed, - } - rosettaTx, rosettaError := formatTransaction(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if len(rosettaTx.Operations) != 3 { - t.Error("Expected 3 operations") - } - if err := assertOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { - t.Error(err) - } - if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() { - t.Error("Invalid transaction") - } - if rosettaTx.Operations[0].Type != common.ExpendGasOperation { - t.Error("Expected 1st operation to be gas") - } - if rosettaTx.Operations[1].Type != common.TransferOperation { - t.Error("Expected 2nd operation to transfer related") - } - if rosettaTx.Operations[1].Metadata != nil { - t.Error("Expected 1st operation to have no metadata") - } - if rosettaTx.Operations[2].Metadata != nil { - t.Error("Expected 2nd operation to have no metadata") - } - if reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) { - t.Error("Expected transaction to have some metadata") - } - if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) { - t.Error("Expected sender to pay gas fee") - } -} - -func TestFormatGenesisTransaction(t *testing.T) { - genesisSpec := getGenesisSpec(0) - testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") - for acc := range genesisSpec.Alloc { - txID := getSpecialCaseTransactionIdentifier(testBlkHash, acc, SpecialGenesisTxID) - tx, rosettaError := formatGenesisTransaction(txID, acc, 0) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(txID, tx.TransactionIdentifier) { - t.Error("expected transaction ID of formatted tx to be same as requested") - } - if len(tx.Operations) != 1 { - t.Error("expected exactly 1 operation") - } - if err := assertOperationTypeUniquenessInvariant(tx.Operations); err != nil { - t.Error(err) - } - if tx.Operations[0].OperationIdentifier.Index != 0 { - t.Error("expected operational ID to be 0") - } - if tx.Operations[0].Type != common.GenesisFundsOperation { - t.Error("expected operation to be genesis funds operations") - } - if tx.Operations[0].Status != common.SuccessOperationStatus.Status { - t.Error("expected successful operation status") - } - } -} - -func TestFormatPreStakingRewardTransactionSuccess(t *testing.T) { - testKey, err := crypto.GenerateKey() - if err != nil { - t.Fatal(err) - } - testAddr := crypto.PubkeyToAddress(testKey.PublicKey) - testBlockSigInfo := &blockSignerInfo{ - signers: map[ethcommon.Address][]bls.SerializedPublicKey{ - testAddr: { // Only care about length for this test - bls.SerializedPublicKey{}, - bls.SerializedPublicKey{}, - }, - }, - totalKeysSigned: 150, - blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), - } - refTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testAddr, SpecialPreStakingRewardTxID) - tx, rosettaError := formatPreStakingRewardTransaction(refTxID, testBlockSigInfo, testAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - if !reflect.DeepEqual(tx.TransactionIdentifier, refTxID) { - t.Errorf("Expected TxID %v got %v", refTxID, tx.TransactionIdentifier) - } - if len(tx.Operations) != 1 { - t.Fatal("Expected exactly 1 operation") - } - if err := assertOperationTypeUniquenessInvariant(tx.Operations); err != nil { - t.Error(err) - } - if tx.Operations[0].OperationIdentifier.Index != 0 { - t.Error("expected operational ID to be 0") - } - 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") - } - - // Expect: myNumberOfSigForBlock * (totalAmountOfRewardsPerBlock / numOfSigsForBlock) to be my block reward amount - refAmount := new(big.Int).Mul(new(big.Int).Quo(stakingNetwork.BlockReward, big.NewInt(150)), big.NewInt(2)) - fmtRefAmount := fmt.Sprintf("%v", refAmount) - if tx.Operations[0].Amount.Value != fmtRefAmount { - t.Errorf("expected operation amount to be %v not %v", fmtRefAmount, tx.Operations[0].Amount.Value) - } -} - -func TestFormatPreStakingRewardTransactionFail(t *testing.T) { - testKey, err := crypto.GenerateKey() - if err != nil { - t.Fatal(err) - } - testAddr := crypto.PubkeyToAddress(testKey.PublicKey) - testBlockSigInfo := &blockSignerInfo{ - signers: map[ethcommon.Address][]bls.SerializedPublicKey{ - testAddr: {}, - }, - totalKeysSigned: 150, - blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), - } - testTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.blockHash, testAddr, SpecialPreStakingRewardTxID) - _, rosettaError := formatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr) - if rosettaError == nil { - t.Fatal("expected rosetta error") - } - if !reflect.DeepEqual(&common.TransactionNotFoundError, rosettaError) { - t.Error("expected transaction not found error") - } - - testBlockSigInfo = &blockSignerInfo{ - signers: map[ethcommon.Address][]bls.SerializedPublicKey{}, - totalKeysSigned: 150, - blockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), - } - _, rosettaError = formatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr) - if rosettaError == nil { - t.Fatal("expected rosetta error") - } - if !reflect.DeepEqual(&common.TransactionNotFoundError, rosettaError) { - t.Error("expected transaction not found error") - } -} - -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 err := assertOperationTypeUniquenessInvariant(tx.Operations); err != nil { - t.Error(err) - } - 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, -) { - // Note that post EIP-155 epoch singer is tested in detailed tests. - signer := hmytypes.HomesteadSigner{} - tx, err := createTestTransaction( - signer, 0, 1, 0, 1e18, big.NewInt(1), []byte("test"), - ) - if err != nil { - t.Fatal(err.Error()) - } - senderAddr, err := tx.SenderAddress() - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, - GasUsed: gasUsed, - } - rosettaTx, rosettaError := formatTransaction(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if len(rosettaTx.Operations) != 2 { - t.Error("Expected 2 operations") - } - if err := assertOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { - t.Error(err) - } - if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() { - t.Error("Invalid transaction") - } - if rosettaTx.Operations[0].Type != common.ExpendGasOperation { - t.Error("Expected 1st operation to be gas") - } - if rosettaTx.Operations[1].Type != common.CrossShardTransferOperation { - t.Error("Expected 2nd operation to cross-shard transfer related") - } - if reflect.DeepEqual(rosettaTx.Operations[1].Metadata, map[string]interface{}{}) { - t.Error("Expected 1st operation to have metadata") - } - if reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) { - t.Error("Expected transaction to have some metadata") - } - if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) { - t.Error("Expected sender to pay gas fee") - } -} - -func TestGetStakingOperationsFromCreateValidator(t *testing.T) { - gasLimit := uint64(1e18) - createValidatorTxDescription := stakingTypes.Description{ - Name: "SuperHero", - Identity: "YouWouldNotKnow", - Website: "Secret Website", - SecurityContact: "LicenseToKill", - Details: "blah blah blah", - } - tx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { - fromKey, _ := crypto.GenerateKey() - return stakingTypes.DirectiveCreateValidator, stakingTypes.CreateValidator{ - Description: createValidatorTxDescription, - MinSelfDelegation: tenOnes, - MaxTotalDelegation: twelveOnes, - ValidatorAddress: crypto.PubkeyToAddress(fromKey.PublicKey), - Amount: tenOnes, - } - }, nil, 0, gasLimit) - if err != nil { - t.Fatal(err.Error()) - } - metadata, err := getMessageFromStakingTx(tx) - if err != nil { - t.Fatal(err.Error()) - } - senderAddr, err := tx.SenderAddress() - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - gasUsed := uint64(1e5) - gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain - GasUsed: gasUsed, - } - refOperations := newOperations(gasFee, senderAccID) - refOperations = append(refOperations, &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{Index: 1}, - RelatedOperations: []*types.OperationIdentifier{ - {Index: 0}, - }, - Type: tx.StakingType().String(), - Status: common.SuccessOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: formatNegativeValue(tenOnes), - Currency: &common.Currency, - }, - Metadata: metadata, - }) - operations, rosettaError := getStakingOperations(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestGetStakingOperationsFromDelegate(t *testing.T) { - gasLimit := uint64(1e18) - senderKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) - validatorKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey) - tx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { - return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{ - DelegatorAddress: senderAddr, - ValidatorAddress: validatorAddr, - Amount: tenOnes, - } - }, senderKey, 0, gasLimit) - if err != nil { - t.Fatal(err.Error()) - } - metadata, err := getMessageFromStakingTx(tx) - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - gasUsed := uint64(1e5) - gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain - GasUsed: gasUsed, - } - refOperations := newOperations(gasFee, senderAccID) - refOperations = append(refOperations, &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{Index: 1}, - RelatedOperations: []*types.OperationIdentifier{ - {Index: 0}, - }, - Type: tx.StakingType().String(), - Status: common.SuccessOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: formatNegativeValue(tenOnes), - Currency: &common.Currency, - }, - Metadata: metadata, - }) - operations, rosettaError := getStakingOperations(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestGetStakingOperationsFromUndelegate(t *testing.T) { - gasLimit := uint64(1e18) - senderKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) - validatorKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey) - tx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { - return stakingTypes.DirectiveUndelegate, stakingTypes.Undelegate{ - DelegatorAddress: senderAddr, - ValidatorAddress: validatorAddr, - Amount: tenOnes, - } - }, senderKey, 0, gasLimit) - if err != nil { - t.Fatal(err.Error()) - } - metadata, err := getMessageFromStakingTx(tx) - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - gasUsed := uint64(1e5) - gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain - GasUsed: gasUsed, - } - refOperations := newOperations(gasFee, senderAccID) - refOperations = append(refOperations, &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{Index: 1}, - RelatedOperations: []*types.OperationIdentifier{ - {Index: 0}, - }, - Type: tx.StakingType().String(), - Status: common.SuccessOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: fmt.Sprintf("0"), - Currency: &common.Currency, - }, - Metadata: metadata, - }) - operations, rosettaError := getStakingOperations(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestGetStakingOperationsFromCollectRewards(t *testing.T) { - gasLimit := uint64(1e18) - senderKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) - tx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { - return stakingTypes.DirectiveCollectRewards, stakingTypes.CollectRewards{ - DelegatorAddress: senderAddr, - } - }, senderKey, 0, gasLimit) - if err != nil { - t.Fatal(err.Error()) - } - metadata, err := getMessageFromStakingTx(tx) - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - gasUsed := uint64(1e5) - gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain - GasUsed: gasUsed, - Logs: []*hmytypes.Log{ - { - Address: senderAddr, - Topics: []ethcommon.Hash{staking.CollectRewardsTopic}, - Data: tenOnes.Bytes(), - }, - }, - } - refOperations := newOperations(gasFee, senderAccID) - refOperations = append(refOperations, &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{Index: 1}, - RelatedOperations: []*types.OperationIdentifier{ - {Index: 0}, - }, - Type: tx.StakingType().String(), - Status: common.SuccessOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: fmt.Sprintf("%v", tenOnes.Uint64()), - Currency: &common.Currency, - }, - Metadata: metadata, - }) - operations, rosettaError := getStakingOperations(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestGetStakingOperationsFromEditValidator(t *testing.T) { - gasLimit := uint64(1e18) - senderKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) - tx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { - return stakingTypes.DirectiveEditValidator, stakingTypes.EditValidator{ - ValidatorAddress: senderAddr, - } - }, senderKey, 0, gasLimit) - if err != nil { - t.Fatal(err.Error()) - } - metadata, err := getMessageFromStakingTx(tx) - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - - gasUsed := uint64(1e5) - gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain - GasUsed: gasUsed, - } - refOperations := newOperations(gasFee, senderAccID) - refOperations = append(refOperations, &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{Index: 1}, - RelatedOperations: []*types.OperationIdentifier{ - {Index: 0}, - }, - Type: tx.StakingType().String(), - Status: common.SuccessOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: fmt.Sprintf("0"), - Currency: &common.Currency, - }, - Metadata: metadata, - }) - operations, rosettaError := getStakingOperations(tx, receipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestNewTransferOperations(t *testing.T) { - signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) - tx, err := createTestTransaction( - signer, 0, 0, 0, 1e18, big.NewInt(1), []byte("test"), - ) - if err != nil { - t.Fatal(err.Error()) - } - senderAddr, err := tx.SenderAddress() - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - receiverAccID, rosettaError := newAccountIdentifier(*tx.To()) - if rosettaError != nil { - t.Fatal(rosettaError) - } - startingOpID := &types.OperationIdentifier{} - - // Test failed 'contract' transaction - refOperations := []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: startingOpID.Index + 1, - }, - RelatedOperations: []*types.OperationIdentifier{ - { - Index: startingOpID.Index, - }, - }, - Type: common.TransferOperation, - Status: common.ContractFailureOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: formatNegativeValue(tx.Value()), - Currency: &common.Currency, - }, - }, - { - OperationIdentifier: &types.OperationIdentifier{ - Index: startingOpID.Index + 2, - }, - RelatedOperations: []*types.OperationIdentifier{ - { - Index: startingOpID.Index + 1, - }, - }, - Type: common.TransferOperation, - Status: common.ContractFailureOperationStatus.Status, - Account: receiverAccID, - Amount: &types.Amount{ - Value: fmt.Sprintf("%v", tx.Value().Uint64()), - Currency: &common.Currency, - }, - }, - } - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusFailed, - } - operations, rosettaError := newTransferOperations(startingOpID, tx, receipt, senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } - - // Test successful plain / contract transaction - refOperations[0].Status = common.SuccessOperationStatus.Status - refOperations[1].Status = common.SuccessOperationStatus.Status - receipt.Status = hmytypes.ReceiptStatusSuccessful - operations, rosettaError = newTransferOperations(startingOpID, tx, receipt, senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestNewCrossShardSenderTransferOperations(t *testing.T) { - signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) - tx, err := createTestTransaction( - signer, 0, 1, 0, 1e18, big.NewInt(1), []byte("data-does-nothing"), - ) - if err != nil { - t.Fatal(err.Error()) - } - senderAddr, err := tx.SenderAddress() - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - startingOpID := &types.OperationIdentifier{} - receiverAccID, rosettaError := newAccountIdentifier(*tx.To()) - if rosettaError != nil { - t.Error(rosettaError) - } - metadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ - From: senderAccID, - To: receiverAccID, - }) - if err != nil { - t.Fatal(err) - } - - refOperations := []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: startingOpID.Index + 1, - }, - RelatedOperations: []*types.OperationIdentifier{ - startingOpID, - }, - Type: common.CrossShardTransferOperation, - Status: common.SuccessOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: formatNegativeValue(tx.Value()), - Currency: &common.Currency, - }, - Metadata: metadata, - }, - } - operations, rosettaError := newCrossShardSenderTransferOperations(startingOpID, tx, senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestFormatCrossShardReceiverTransaction(t *testing.T) { - signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) - tx, err := createTestTransaction( - signer, 0, 1, 0, 1e18, big.NewInt(1), []byte{}, - ) - if err != nil { - t.Fatal(err.Error()) - } - senderAddr, err := tx.SenderAddress() - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - receiverAccID, rosettaError := newAccountIdentifier(*tx.To()) - if rosettaError != nil { - t.Fatal(rosettaError) - } - cxReceipt := &hmytypes.CXReceipt{ - TxHash: tx.Hash(), - From: senderAddr, - To: tx.To(), - ShardID: 0, - ToShardID: 1, - Amount: tx.Value(), - } - opMetadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ - From: senderAccID, - To: receiverAccID, - }) - if err != nil { - t.Error(err) - } - - refCxID := &types.TransactionIdentifier{Hash: tx.Hash().String()} - refOperations := []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: 0, // There is no gas expenditure for cross-shard payout - }, - Type: common.CrossShardTransferOperation, - Status: common.SuccessOperationStatus.Status, - Account: receiverAccID, - Amount: &types.Amount{ - Value: fmt.Sprintf("%v", tx.Value().Uint64()), - Currency: &common.Currency, - }, - Metadata: opMetadata, - }, - } - to := tx.ToShardID() - from := tx.ShardID() - refMetadata, err := types.MarshalMap(TransactionMetadata{ - CrossShardIdentifier: refCxID, - ToShardID: &to, - FromShardID: &from, - }) - refRosettaTx := &types.Transaction{ - TransactionIdentifier: refCxID, - Operations: refOperations, - Metadata: refMetadata, - } - rosettaTx, rosettaError := formatCrossShardReceiverTransaction(cxReceipt) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(rosettaTx, refRosettaTx) { - t.Errorf("Expected transaction to be %v not %v", refRosettaTx, rosettaTx) - } - if err := assertOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { - t.Error(err) - } -} - -func TestNewContractCreationOperations(t *testing.T) { - dummyContractKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - chainID := params.TestChainConfig.ChainID - signer := hmytypes.NewEIP155Signer(chainID) - tx, err := createTestContractCreationTransaction( - signer, 0, 0, 1e18, []byte("test"), - ) - if err != nil { - t.Fatal(err.Error()) - } - senderAddr, err := tx.SenderAddress() - if err != nil { - t.Fatal(err.Error()) - } - senderAccID, rosettaError := newAccountIdentifier(senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - startingOpID := &types.OperationIdentifier{} - - // Test failed contract creation - contractAddr := crypto.PubkeyToAddress(dummyContractKey.PublicKey) - contractAddressID, rosettaError := newAccountIdentifier(contractAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - refOperations := []*types.Operation{ - { - OperationIdentifier: &types.OperationIdentifier{ - Index: startingOpID.Index + 1, - }, - RelatedOperations: []*types.OperationIdentifier{ - startingOpID, - }, - Type: common.ContractCreationOperation, - Status: common.ContractFailureOperationStatus.Status, - Account: senderAccID, - Amount: &types.Amount{ - Value: formatNegativeValue(tx.Value()), - Currency: &common.Currency, - }, - Metadata: map[string]interface{}{ - "contract_address": contractAddressID, - }, - }, - } - receipt := &hmytypes.Receipt{ - Status: hmytypes.ReceiptStatusFailed, - ContractAddress: contractAddr, - } - operations, rosettaError := newContractCreationOperations(startingOpID, tx, receipt, senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } - - // Test successful contract creation - refOperations[0].Status = common.SuccessOperationStatus.Status - receipt.Status = hmytypes.ReceiptStatusSuccessful // Indicate successful tx - operations, rosettaError = newContractCreationOperations(startingOpID, tx, receipt, senderAddr) - if rosettaError != nil { - t.Fatal(rosettaError) - } - if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) - } - if err := assertOperationTypeUniquenessInvariant(operations); err != nil { - t.Error(err) - } -} - -func TestNewAccountIdentifier(t *testing.T) { - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - addr := crypto.PubkeyToAddress(key.PublicKey) - b32Addr, err := internalCommon.AddressToBech32(addr) - if err != nil { - t.Fatalf(err.Error()) - } - metadata, err := types.MarshalMap(AccountMetadata{Address: addr.String()}) - if err != nil { - t.Fatalf(err.Error()) - } - - referenceAccID := &types.AccountIdentifier{ - Address: b32Addr, - Metadata: metadata, - } - testAccID, rosettaError := newAccountIdentifier(addr) - if rosettaError != nil { - t.Fatalf("unexpected rosetta error: %v", rosettaError) - } - if !reflect.DeepEqual(referenceAccID, testAccID) { - t.Errorf("reference ID %v != testID %v", referenceAccID, testAccID) - } -} - -func TestGetAddress(t *testing.T) { - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf(err.Error()) - } - addr := crypto.PubkeyToAddress(key.PublicKey) - b32Addr, err := internalCommon.AddressToBech32(addr) - if err != nil { - t.Fatalf(err.Error()) - } - testAccID := &types.AccountIdentifier{ - Address: b32Addr, - } - - testAddr, err := getAddress(testAccID) - if err != nil { - t.Fatal(err) - } - if testAddr != addr { - t.Errorf("expected %v to be %v", testAddr.String(), addr.String()) - } - - defaultAddr := ethcommon.Address{} - testAddr, err = getAddress(nil) - if err == nil { - t.Error("expected err for nil identifier") - } - if testAddr != defaultAddr { - t.Errorf("expected errored addres to be %v not %v", defaultAddr.String(), testAddr.String()) - } -} - -func TestNewOperations(t *testing.T) { - accountID := &types.AccountIdentifier{ - Address: "test-address", - } - gasFee := big.NewInt(int64(1e18)) - amount := &types.Amount{ - Value: formatNegativeValue(gasFee), - Currency: &common.Currency, - } - - ops := newOperations(gasFee, accountID) - if len(ops) != 1 { - t.Fatalf("Expected new operations to be of length 1") - } - if !reflect.DeepEqual(ops[0].Account, accountID) { - t.Errorf("Expected account ID to be %v not %v", accountID, ops[0].OperationIdentifier) - } - if !reflect.DeepEqual(ops[0].Amount, amount) { - t.Errorf("Expected amount to be %v not %v", amount, ops[0].Amount) - } - if ops[0].Type != common.ExpendGasOperation { - t.Errorf("Expected operation to be %v not %v", common.ExpendGasOperation, ops[0].Type) - } - if ops[0].OperationIdentifier.Index != 0 { - t.Errorf("Expected operational ID to be of index 0") - } - if ops[0].Status != common.SuccessOperationStatus.Status { - t.Errorf("Expected operation status to be %v", common.SuccessOperationStatus.Status) - } -} - -func TestFindLogsWithTopic(t *testing.T) { - tests := []struct { - receipt *hmytypes.Receipt - topic ethcommon.Hash - expectedResponse []*hmytypes.Log - }{ - // test 0 - { - receipt: &hmytypes.Receipt{ - Logs: []*hmytypes.Log{ - { - Topics: []ethcommon.Hash{ - staking.IsValidatorKey, - staking.IsValidator, - }, - }, - { - Topics: []ethcommon.Hash{ - crypto.Keccak256Hash([]byte("test")), - }, - }, - { - Topics: []ethcommon.Hash{ - staking.CollectRewardsTopic, - }, - }, - }, - }, - topic: staking.IsValidatorKey, - expectedResponse: []*hmytypes.Log{ - { - Topics: []ethcommon.Hash{ - staking.IsValidatorKey, - staking.IsValidator, - }, - }, - }, - }, - // test 1 - { - receipt: &hmytypes.Receipt{ - Logs: []*hmytypes.Log{ - { - Topics: []ethcommon.Hash{ - staking.IsValidatorKey, - staking.IsValidator, - }, - }, - { - Topics: []ethcommon.Hash{ - crypto.Keccak256Hash([]byte("test")), - }, - }, - { - Topics: []ethcommon.Hash{ - staking.CollectRewardsTopic, - }, - }, - }, - }, - topic: staking.CollectRewardsTopic, - expectedResponse: []*hmytypes.Log{ - { - Topics: []ethcommon.Hash{ - staking.CollectRewardsTopic, - }, - }, - }, - }, - // test 2 - { - receipt: &hmytypes.Receipt{ - Logs: []*hmytypes.Log{ - { - Topics: []ethcommon.Hash{ - staking.IsValidatorKey, - }, - }, - { - Topics: []ethcommon.Hash{ - crypto.Keccak256Hash([]byte("test")), - }, - }, - { - Topics: []ethcommon.Hash{ - staking.CollectRewardsTopic, - }, - }, - }, - }, - topic: staking.IsValidator, - expectedResponse: []*hmytypes.Log{}, - }, - } - - for i, test := range tests { - response := findLogsWithTopic(test.receipt, test.topic) - if !reflect.DeepEqual(test.expectedResponse, response) { - t.Errorf("Failed test %v, expected %v, got %v", i, test.expectedResponse, response) - } - } -} - -func TestGetPseudoTransactionForGenesis(t *testing.T) { - genesisSpec := core.NewGenesisSpec(nodeconfig.Testnet, 0) - txs := getPseudoTransactionForGenesis(genesisSpec) - for acc := range genesisSpec.Alloc { - found := false - for _, tx := range txs { - if acc == *tx.To() { - found = true - break - } - } - if !found { - t.Error("unable to find genesis account in generated pseudo transactions") - } - } -} - -func TestSpecialCaseTransactionIdentifier(t *testing.T) { - testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") - testB32Address := "one10g7kfque6ew2jjfxxa6agkdwk4wlyjuncp6gwz" - testAddress := internalCommon.MustBech32ToAddress(testB32Address) - refTxID := &types.TransactionIdentifier{ - Hash: fmt.Sprintf("%v_%v_%v", testBlkHash.String(), testB32Address, SpecialGenesisTxID.String()), - } - specialTxID := getSpecialCaseTransactionIdentifier( - testBlkHash, testAddress, SpecialGenesisTxID, - ) - if !reflect.DeepEqual(refTxID, specialTxID) { - t.Fatal("invalid for mate for special case TxID") - } - unpackedBlkHash, unpackedAddress, rosettaError := unpackSpecialCaseTransactionIdentifier( - specialTxID, SpecialGenesisTxID, - ) - if rosettaError != nil { - t.Fatal(rosettaError) - } - 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: ""}, SpecialGenesisTxID, - ) - if rosettaError == nil { - t.Fatal("expected rosetta error") - } - if rosettaError.Code != common.CatchAllError.Code { - t.Error("expected error code to be catch call error") - } -} diff --git a/rosetta/services/construction_check.go b/rosetta/services/construction_check.go index 6b86c40ed..7ae31bb8e 100644 --- a/rosetta/services/construction_check.go +++ b/rosetta/services/construction_check.go @@ -183,11 +183,11 @@ func (s *ConstructAPI) ConstructionMetadata( if options.GasPriceMultiplier != nil && *options.GasPriceMultiplier > 1 { gasMul = *options.GasPriceMultiplier } - suggestedFee, suggestedPrice := getSuggestedFeeAndPrice(gasMul, new(big.Int).SetUint64(estGasUsed)) + sugNativeFee, sugNativePrice := getSuggestedNativeFeeAndPrice(gasMul, new(big.Int).SetUint64(estGasUsed)) metadata, err := types.MarshalMap(ConstructMetadata{ Nonce: nonce, - GasPrice: suggestedPrice, + GasPrice: sugNativePrice, GasLimit: estGasUsed, Transaction: options.TransactionMetadata, }) @@ -198,12 +198,12 @@ func (s *ConstructAPI) ConstructionMetadata( } return &types.ConstructionMetadataResponse{ Metadata: metadata, - SuggestedFee: suggestedFee, + SuggestedFee: sugNativeFee, }, nil } -// getSuggestedFeeAndPrice .. -func getSuggestedFeeAndPrice( +// getSuggestedNativeFeeAndPrice .. +func getSuggestedNativeFeeAndPrice( gasMul float64, estGasUsed *big.Int, ) ([]*types.Amount, *big.Int) { if estGasUsed == nil { @@ -218,7 +218,7 @@ func getSuggestedFeeAndPrice( return []*types.Amount{ { Value: fmt.Sprintf("%v", new(big.Int).Mul(gasPrice, estGasUsed)), - Currency: &common.Currency, + Currency: &common.NativeCurrency, }, }, gasPrice } diff --git a/rosetta/services/construction_check_test.go b/rosetta/services/construction_check_test.go index b2ac7fdeb..548c5531e 100644 --- a/rosetta/services/construction_check_test.go +++ b/rosetta/services/construction_check_test.go @@ -28,7 +28,7 @@ func TestConstructMetadataOptions(t *testing.T) { { Metadata: ConstructMetadataOptions{ TransactionMetadata: refTxMedata, - OperationType: common.TransferOperation, + OperationType: common.TransferNativeOperation, GasPriceMultiplier: nil, }, ExpectError: false, @@ -36,7 +36,7 @@ func TestConstructMetadataOptions(t *testing.T) { { Metadata: ConstructMetadataOptions{ TransactionMetadata: refTxMedata, - OperationType: common.TransferOperation, + OperationType: common.TransferNativeOperation, GasPriceMultiplier: &refGasPrice, }, ExpectError: false, @@ -44,7 +44,7 @@ func TestConstructMetadataOptions(t *testing.T) { { Metadata: ConstructMetadataOptions{ TransactionMetadata: nil, - OperationType: common.TransferOperation, + OperationType: common.TransferNativeOperation, GasPriceMultiplier: &refGasPrice, }, ExpectError: true, @@ -52,7 +52,7 @@ func TestConstructMetadataOptions(t *testing.T) { { Metadata: ConstructMetadataOptions{ TransactionMetadata: nil, - OperationType: common.TransferOperation, + OperationType: common.TransferNativeOperation, GasPriceMultiplier: nil, }, ExpectError: true, @@ -170,7 +170,7 @@ func TestConstructMetadata(t *testing.T) { } } -func TestGetSuggestedFeeAndPrice(t *testing.T) { +func TestGetSuggestedNativeFeeAndPrice(t *testing.T) { refEstGasUsed := big.NewInt(1000000) cases := []struct { @@ -218,7 +218,7 @@ func TestGetSuggestedFeeAndPrice(t *testing.T) { } for i, test := range cases { - refAmounts, refPrice := getSuggestedFeeAndPrice(test.GasMul, test.EstGasUsed) + refAmounts, refPrice := getSuggestedNativeFeeAndPrice(test.GasMul, test.EstGasUsed) if len(refAmounts) != 1 { t.Errorf("expect exactly 1 amount for case %v", i) continue diff --git a/rosetta/services/construction_create_test.go b/rosetta/services/construction_create_test.go index 8acc60692..815755785 100644 --- a/rosetta/services/construction_create_test.go +++ b/rosetta/services/construction_create_test.go @@ -11,6 +11,7 @@ import ( hmytypes "github.com/harmony-one/harmony/core/types" stakingTypes "github.com/harmony-one/harmony/staking/types" + "github.com/harmony-one/harmony/test/helpers" ) func TestUnpackWrappedTransactionFromString(t *testing.T) { @@ -27,8 +28,8 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { signer := hmytypes.NewEIP155Signer(big.NewInt(0)) // Test plain transactions - tx, err := createTestTransaction( - signer, 0, 1, 2, refEstGasUsed.Uint64(), big.NewInt(1e10), []byte{0x01, 0x02}, + tx, err := helpers.CreateTestTransaction( + signer, 0, 1, 2, refEstGasUsed.Uint64(), gasPrice, big.NewInt(1e10), []byte{0x01, 0x02}, ) if err != nil { t.Fatal(err) @@ -62,13 +63,13 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { if err != nil { t.Fatalf(err.Error()) } - stx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { + stx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{ DelegatorAddress: refAddr, ValidatorAddress: crypto.PubkeyToAddress(receiverKey.PublicKey), Amount: tenOnes, } - }, refKey, 10, refEstGasUsed.Uint64()) + }, refKey, 10, refEstGasUsed.Uint64(), gasPrice) if err != nil { t.Fatal(err) } diff --git a/rosetta/services/construction_parse.go b/rosetta/services/construction_parse.go index 288e489d4..259ff0156 100644 --- a/rosetta/services/construction_parse.go +++ b/rosetta/services/construction_parse.go @@ -41,11 +41,11 @@ func parseUnsignedTransaction( intendedReceipt := &hmyTypes.Receipt{ GasUsed: tx.Gas(), } - formattedTx, rosettaError := formatTransaction(tx, intendedReceipt) + formattedTx, rosettaError := FormatTransaction(tx, intendedReceipt) if rosettaError != nil { return nil, rosettaError } - tempAccID, rosettaError := newAccountIdentifier(DefaultSenderAddress) + tempAccID, rosettaError := newAccountIdentifier(FormatDefaultSenderAddress) if rosettaError != nil { return nil, rosettaError } @@ -82,7 +82,7 @@ func parseSignedTransaction( intendedReceipt := &hmyTypes.Receipt{ GasUsed: tx.Gas(), } - formattedTx, rosettaError := formatTransaction(tx, intendedReceipt) + formattedTx, rosettaError := FormatTransaction(tx, intendedReceipt) if rosettaError != nil { return nil, rosettaError } diff --git a/rosetta/services/construction_parse_test.go b/rosetta/services/construction_parse_test.go index 25bd5f804..1c7ed3c6e 100644 --- a/rosetta/services/construction_parse_test.go +++ b/rosetta/services/construction_parse_test.go @@ -11,6 +11,7 @@ import ( hmytypes "github.com/harmony-one/harmony/core/types" internalCommon "github.com/harmony-one/harmony/internal/common" stakingTypes "github.com/harmony-one/harmony/staking/types" + "github.com/harmony-one/harmony/test/helpers" ) var ( @@ -20,8 +21,8 @@ var ( func TestParseUnsignedTransaction(t *testing.T) { refEstGasUsed := big.NewInt(100000) - testTx, err := createTestTransaction( - tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), big.NewInt(1e18), []byte{}, + testTx, err := helpers.CreateTestTransaction( + tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), gasPrice, big.NewInt(1e18), []byte{}, ) if err != nil { t.Fatal(err) @@ -37,7 +38,7 @@ func TestParseUnsignedTransaction(t *testing.T) { refTestReceipt := &hmytypes.Receipt{ GasUsed: testTx.Gas(), } - refFormattedTx, rosettaError := formatTransaction(testTx, refTestReceipt) + refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt) if rosettaError != nil { t.Fatal(rosettaError) } @@ -83,8 +84,8 @@ func TestParseUnsignedTransactionStaking(t *testing.T) { func TestParseSignedTransaction(t *testing.T) { refEstGasUsed := big.NewInt(100000) - testTx, err := createTestTransaction( - tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), big.NewInt(1e18), []byte{}, + testTx, err := helpers.CreateTestTransaction( + tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), gasPrice, big.NewInt(1e18), []byte{}, ) if err != nil { t.Fatal(err) @@ -100,7 +101,7 @@ func TestParseSignedTransaction(t *testing.T) { refTestReceipt := &hmytypes.Receipt{ GasUsed: testTx.Gas(), } - refFormattedTx, rosettaError := formatTransaction(testTx, refTestReceipt) + refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt) if rosettaError != nil { t.Fatal(rosettaError) } diff --git a/rosetta/services/mempool.go b/rosetta/services/mempool.go index 48b5b1355..54b525280 100644 --- a/rosetta/services/mempool.go +++ b/rosetta/services/mempool.go @@ -25,7 +25,7 @@ func NewMempoolAPI(hmy *hmy.Harmony) server.MempoolAPIServicer { } } -// Mempool ... +// Mempool implements the /mempool endpoint. func (s *MempoolAPI) Mempool( ctx context.Context, req *types.NetworkRequest, ) (*types.MempoolResponse, *types.Error) { @@ -50,7 +50,7 @@ func (s *MempoolAPI) Mempool( }, nil } -// MempoolTransaction ... +// MempoolTransaction implements the /mempool/transaction endpoint. func (s *MempoolAPI) MempoolTransaction( ctx context.Context, req *types.MempoolTransactionRequest, ) (*types.MempoolTransactionResponse, *types.Error) { @@ -84,7 +84,7 @@ func (s *MempoolAPI) MempoolTransaction( GasUsed: poolTx.Gas(), } - respTx, err := formatTransaction(poolTx, estReceipt) + respTx, err := FormatTransaction(poolTx, estReceipt) if err != nil { return nil, err } diff --git a/rosetta/services/transaction_construction.go b/rosetta/services/tx_construction.go similarity index 84% rename from rosetta/services/transaction_construction.go rename to rosetta/services/tx_construction.go index d878e523b..887486754 100644 --- a/rosetta/services/transaction_construction.go +++ b/rosetta/services/tx_construction.go @@ -1,6 +1,7 @@ package services import ( + "encoding/json" "fmt" "github.com/coinbase/rosetta-sdk-go/types" @@ -11,6 +12,30 @@ import ( "github.com/harmony-one/harmony/rosetta/common" ) +// TransactionMetadata contains all (optional) information for a transaction. +type TransactionMetadata struct { + // CrossShardIdentifier is the transaction identifier on the from/source shard + CrossShardIdentifier *types.TransactionIdentifier `json:"cross_shard_transaction_identifier,omitempty"` + ToShardID *uint32 `json:"to_shard,omitempty"` + FromShardID *uint32 `json:"from_shard,omitempty"` + Data *string `json:"data,omitempty"` + Logs []*hmyTypes.Log `json:"logs,omitempty"` +} + +// UnmarshalFromInterface .. +func (t *TransactionMetadata) UnmarshalFromInterface(metaData interface{}) error { + var args TransactionMetadata + dat, err := json.Marshal(metaData) + if err != nil { + return err + } + if err := json.Unmarshal(dat, &args); err != nil { + return err + } + *t = args + return nil +} + // ConstructTransaction object (unsigned). // TODO (dm): implement staking transaction construction func ConstructTransaction( @@ -30,7 +55,7 @@ func ConstructTransaction( var tx hmyTypes.PoolTransaction switch components.Type { - case common.CrossShardTransferOperation: + case common.CrossShardTransferNativeOperation: if tx, rosettaError = constructCrossShardTransaction(components, metadata, sourceShardID); rosettaError != nil { return nil, rosettaError } @@ -38,7 +63,7 @@ func ConstructTransaction( if tx, rosettaError = constructContractCreationTransaction(components, metadata, sourceShardID); rosettaError != nil { return nil, rosettaError } - case common.TransferOperation: + case common.TransferNativeOperation: if tx, rosettaError = constructPlainTransaction(components, metadata, sourceShardID); rosettaError != nil { return nil, rosettaError } diff --git a/rosetta/services/transaction_construction_test.go b/rosetta/services/tx_construction_test.go similarity index 96% rename from rosetta/services/transaction_construction_test.go rename to rosetta/services/tx_construction_test.go index 988d1f81d..966f1e17c 100644 --- a/rosetta/services/transaction_construction_test.go +++ b/rosetta/services/tx_construction_test.go @@ -27,7 +27,7 @@ func TestConstructPlainTransaction(t *testing.T) { refDataBytes := []byte{0xEE, 0xEE, 0xEE} refData := hexutil.Encode(refDataBytes) refComponents := &OperationComponents{ - Type: common.TransferOperation, + Type: common.TransferNativeOperation, From: refFrom, To: refTo, Amount: big.NewInt(12000), @@ -116,7 +116,7 @@ func TestConstructPlainTransaction(t *testing.T) { // test invalid receiver _, rosettaError = constructPlainTransaction(&OperationComponents{ - Type: common.TransferOperation, + Type: common.TransferNativeOperation, From: refFrom, To: nil, Amount: big.NewInt(12000), @@ -126,7 +126,7 @@ func TestConstructPlainTransaction(t *testing.T) { t.Error("expected error") } _, rosettaError = constructPlainTransaction(&OperationComponents{ - Type: common.TransferOperation, + Type: common.TransferNativeOperation, From: refFrom, To: &types.AccountIdentifier{ Address: "", @@ -140,7 +140,7 @@ func TestConstructPlainTransaction(t *testing.T) { // test valid nil sender _, rosettaError = constructPlainTransaction(&OperationComponents{ - Type: common.TransferOperation, + Type: common.TransferNativeOperation, From: nil, To: refTo, Amount: big.NewInt(12000), @@ -179,7 +179,7 @@ func TestConstructCrossShardTransaction(t *testing.T) { refDataBytes := []byte{0xEE, 0xEE, 0xEE} refData := hexutil.Encode(refDataBytes) refComponents := &OperationComponents{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, From: refFrom, To: refTo, Amount: big.NewInt(12000), @@ -238,7 +238,7 @@ func TestConstructCrossShardTransaction(t *testing.T) { // test invalid receiver _, rosettaError = constructCrossShardTransaction(&OperationComponents{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, From: refFrom, To: nil, Amount: big.NewInt(12000), @@ -248,7 +248,7 @@ func TestConstructCrossShardTransaction(t *testing.T) { t.Error("expected error") } _, rosettaError = constructCrossShardTransaction(&OperationComponents{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, From: refFrom, To: &types.AccountIdentifier{ Address: "", @@ -262,7 +262,7 @@ func TestConstructCrossShardTransaction(t *testing.T) { // test valid nil sender _, rosettaError = constructCrossShardTransaction(&OperationComponents{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, From: nil, To: refTo, Amount: big.NewInt(12000), @@ -432,7 +432,7 @@ func TestConstructTransaction(t *testing.T) { // test valid cross-shard transfer (negative test cases are in TestConstructCrossShardTransaction) generalTx, rosettaError := ConstructTransaction(&OperationComponents{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, From: refFrom, To: refTo, Amount: big.NewInt(12000), @@ -485,7 +485,7 @@ func TestConstructTransaction(t *testing.T) { // test valid transfer (negative test cases are in TestConstructPlainTransaction) generalTx, rosettaError = ConstructTransaction(&OperationComponents{ - Type: common.TransferOperation, + Type: common.TransferNativeOperation, From: refFrom, To: refTo, Amount: big.NewInt(12000), @@ -512,7 +512,7 @@ func TestConstructTransaction(t *testing.T) { // test invalid sender shard badShard := refShard + refToShard + 1 _, rosettaError = ConstructTransaction(&OperationComponents{ - Type: common.TransferOperation, + Type: common.TransferNativeOperation, From: refFrom, To: refTo, Amount: big.NewInt(12000), diff --git a/rosetta/services/tx_format.go b/rosetta/services/tx_format.go new file mode 100644 index 000000000..f23dc87ba --- /dev/null +++ b/rosetta/services/tx_format.go @@ -0,0 +1,277 @@ +package services + +import ( + "encoding/hex" + "fmt" + "math/big" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + + hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/hmy" + internalCommon "github.com/harmony-one/harmony/internal/common" + "github.com/harmony-one/harmony/rosetta/common" + stakingNetwork "github.com/harmony-one/harmony/staking/network" + stakingTypes "github.com/harmony-one/harmony/staking/types" +) + +var ( + // FormatDefaultSenderAddress .. + FormatDefaultSenderAddress = ethcommon.HexToAddress("0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE") +) + +// FormatTransaction for staking, cross-shard sender, and plain transactions +func FormatTransaction( + tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt, +) (fmtTx *types.Transaction, rosettaError *types.Error) { + var operations []*types.Operation + var isCrossShard, isStaking bool + var toShard uint32 + + switch tx.(type) { + case *stakingTypes.StakingTransaction: + isStaking = true + stakingTx := tx.(*stakingTypes.StakingTransaction) + operations, rosettaError = GetOperationsFromStakingTransaction(stakingTx, receipt) + if rosettaError != nil { + return nil, rosettaError + } + isCrossShard = false + toShard = stakingTx.ShardID() + case *hmytypes.Transaction: + isStaking = false + plainTx := tx.(*hmytypes.Transaction) + operations, rosettaError = GetNativeOperationsFromTransaction(plainTx, receipt) + if rosettaError != nil { + return nil, rosettaError + } + isCrossShard = plainTx.ShardID() != plainTx.ToShardID() + toShard = plainTx.ToShardID() + default: + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "unknown transaction type", + }) + } + fromShard := tx.ShardID() + txID := &types.TransactionIdentifier{Hash: tx.Hash().String()} + + // Set all possible metadata + var txMetadata TransactionMetadata + if isCrossShard { + txMetadata.CrossShardIdentifier = txID + txMetadata.ToShardID = &toShard + txMetadata.FromShardID = &fromShard + } + if len(tx.Data()) > 0 && !isStaking { + hexData := hex.EncodeToString(tx.Data()) + txMetadata.Data = &hexData + txMetadata.Logs = receipt.Logs + } + metadata, err := types.MarshalMap(txMetadata) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + + return &types.Transaction{ + TransactionIdentifier: txID, + Operations: operations, + Metadata: metadata, + }, nil +} + +// FormatCrossShardReceiverTransaction for cross-shard payouts on destination shard +func FormatCrossShardReceiverTransaction( + cxReceipt *hmytypes.CXReceipt, +) (txs *types.Transaction, rosettaError *types.Error) { + ctxID := &types.TransactionIdentifier{Hash: cxReceipt.TxHash.String()} + senderAccountID, rosettaError := newAccountIdentifier(cxReceipt.From) + if rosettaError != nil { + return nil, rosettaError + } + receiverAccountID, rosettaError := newAccountIdentifier(*cxReceipt.To) + if rosettaError != nil { + return nil, rosettaError + } + metadata, err := types.MarshalMap(TransactionMetadata{ + CrossShardIdentifier: ctxID, + ToShardID: &cxReceipt.ToShardID, + FromShardID: &cxReceipt.ShardID, + }) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + opMetadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ + From: senderAccountID, + To: receiverAccountID, + }) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + + return &types.Transaction{ + TransactionIdentifier: ctxID, + Metadata: metadata, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, // There is no gas expenditure for cross-shard transaction payout + }, + Type: common.CrossShardTransferNativeOperation, + Status: common.SuccessOperationStatus.Status, + Account: receiverAccountID, + Amount: &types.Amount{ + Value: cxReceipt.Amount.String(), + Currency: &common.NativeCurrency, + }, + Metadata: opMetadata, + }, + }, + }, nil +} + +// FormatGenesisTransaction for genesis block's initial balances +func FormatGenesisTransaction( + txID *types.TransactionIdentifier, targetAddr ethcommon.Address, shardID uint32, +) (fmtTx *types.Transaction, rosettaError *types.Error) { + var b32Addr string + targetB32Addr := internalCommon.MustAddressToBech32(targetAddr) + for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(shardID)) { + if tx.To() == nil { + return nil, common.NewError(common.CatchAllError, nil) + } + b32Addr = internalCommon.MustAddressToBech32(*tx.To()) + if targetB32Addr == b32Addr { + accID, rosettaError := newAccountIdentifier(*tx.To()) + if rosettaError != nil { + return nil, rosettaError + } + return &types.Transaction{ + TransactionIdentifier: txID, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.GenesisFundsOperation, + Status: common.SuccessOperationStatus.Status, + Account: accID, + Amount: &types.Amount{ + Value: tx.Value().String(), + Currency: &common.NativeCurrency, + }, + }, + }, + }, nil + } + } + return nil, &common.TransactionNotFoundError +} + +// FormatPreStakingRewardTransaction for block rewards in pre-staking era for a given Bech-32 address. +func FormatPreStakingRewardTransaction( + txID *types.TransactionIdentifier, blockSigInfo *hmy.DetailedBlockSignerInfo, address ethcommon.Address, +) (*types.Transaction, *types.Error) { + signatures, ok := blockSigInfo.Signers[address] + if !ok || len(signatures) == 0 { + return nil, &common.TransactionNotFoundError + } + accID, rosettaError := newAccountIdentifier(address) + if rosettaError != nil { + return nil, rosettaError + } + + // Calculate rewards exactly like `AccumulateRewardsAndCountSigs` but short circuit when possible. + // WARNING: must do calculation in the order of the committee to get accurate values. + i := 0 + last := big.NewInt(0) + rewardsForThisBlock := big.NewInt(0) + count := big.NewInt(int64(blockSigInfo.TotalKeysSigned)) + for _, slot := range blockSigInfo.Committee { + rewardsForThisAddr := big.NewInt(0) + if keys, ok := blockSigInfo.Signers[slot.EcdsaAddress]; ok { + for range keys { + cur := big.NewInt(0) + cur.Mul(stakingNetwork.BlockReward, big.NewInt(int64(i+1))).Div(cur, count) + reward := big.NewInt(0).Sub(cur, last) + rewardsForThisAddr = new(big.Int).Add(reward, rewardsForThisAddr) + last = cur + i++ + } + } + if slot.EcdsaAddress == address { + rewardsForThisBlock = rewardsForThisAddr + if !(rewardsForThisAddr.Cmp(big.NewInt(0)) > 0) { + return nil, common.NewError(common.SanityCheckError, map[string]interface{}{ + "message": "expected non-zero block reward in pre-staking era for block signer", + }) + } + break + } + } + + return &types.Transaction{ + TransactionIdentifier: txID, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.PreStakingBlockRewardOperation, + Status: common.SuccessOperationStatus.Status, + Account: accID, + Amount: &types.Amount{ + Value: rewardsForThisBlock.String(), + Currency: &common.NativeCurrency, + }, + }, + }, + }, 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: payout.String(), + Currency: &common.NativeCurrency, + }, + }, + }, + }, nil + +} + +// negativeBigValue formats a transaction value as a string +func negativeBigValue(num *big.Int) string { + value := "0" + if num != nil && num.Cmp(big.NewInt(0)) != 0 { + value = fmt.Sprintf("-%v", new(big.Int).Abs(num)) + } + return value +} diff --git a/rosetta/services/tx_format_test.go b/rosetta/services/tx_format_test.go new file mode 100644 index 000000000..5a9db4c48 --- /dev/null +++ b/rosetta/services/tx_format_test.go @@ -0,0 +1,504 @@ +package services + +import ( + "crypto/ecdsa" + "fmt" + "math/big" + "reflect" + "testing" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/crypto/bls" + "github.com/harmony-one/harmony/hmy" + "github.com/harmony-one/harmony/internal/params" + "github.com/harmony-one/harmony/rosetta/common" + "github.com/harmony-one/harmony/shard" + stakingNetwork "github.com/harmony-one/harmony/staking/network" + stakingTypes "github.com/harmony-one/harmony/staking/types" + "github.com/harmony-one/harmony/test/helpers" +) + +// Invariant: A transaction can only contain 1 type of native operation(s) other than gas expenditure. +func assertNativeOperationTypeUniquenessInvariant(operations []*types.Operation) error { + foundType := "" + for _, op := range operations { + if op.Type == common.ExpendGasOperation { + continue + } + if foundType == "" { + foundType = op.Type + } + if op.Type != foundType { + return fmt.Errorf("found more than 1 type in given set of operations") + } + } + return nil +} + +// Note that this test only checks the general format of each type transaction on Harmony. +// The detailed operation checks for each type of transaction is done in separate unit tests. +func TestFormatTransactionIntegration(t *testing.T) { + gasLimit := uint64(1e18) + gasUsed := uint64(1e5) + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + receiverKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + + testFormatStakingTransaction(t, gasLimit, gasUsed, senderKey, receiverKey) + testFormatPlainTransaction(t, gasLimit, gasUsed, senderKey, receiverKey) + // Note that cross-shard receiver operations/transactions are formatted via + // FormatCrossShardReceiverTransaction, thus, it is not tested here -- but tested on its own. + testFormatCrossShardSenderTransaction(t, gasLimit, gasUsed, senderKey, receiverKey) +} + +func testFormatStakingTransaction( + t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey, +) { + senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) + receiverAddr := crypto.PubkeyToAddress(receiverKey.PublicKey) + tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{ + DelegatorAddress: senderAddr, + ValidatorAddress: receiverAddr, + Amount: tenOnes, + } + }, senderKey, 0, gasLimit, gasPrice) + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, + GasUsed: gasUsed, + } + rosettaTx, rosettaError := FormatTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + if len(rosettaTx.Operations) != 2 { + t.Error("Expected 2 operations") + } + if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { + t.Error(err) + } + if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() { + t.Error("Invalid transaction") + } + if rosettaTx.Operations[0].Type != common.ExpendGasOperation { + t.Error("Expected 1st operation to be gas type") + } + if rosettaTx.Operations[1].Type != tx.StakingType().String() { + t.Error("Expected 2nd operation to be staking type") + } + if reflect.DeepEqual(rosettaTx.Operations[1].Metadata, map[string]interface{}{}) { + t.Error("Expected staking operation to have some metadata") + } + if !reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) { + t.Error("Expected transaction to have no metadata") + } + if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) { + t.Error("Expected sender to pay gas fee") + } +} + +func testFormatPlainTransaction( + t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey, +) { + // Note that post EIP-155 epoch singer is tested in detailed tests. + signer := hmytypes.HomesteadSigner{} + tx, err := helpers.CreateTestTransaction( + signer, 0, 0, 0, 1e18, gasPrice, big.NewInt(1), []byte("test"), + ) + if err != nil { + t.Fatal(err.Error()) + } + senderAddr, err := tx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, + GasUsed: gasUsed, + } + rosettaTx, rosettaError := FormatTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if len(rosettaTx.Operations) != 3 { + t.Error("Expected 3 operations") + } + if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { + t.Error(err) + } + if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() { + t.Error("Invalid transaction") + } + if rosettaTx.Operations[0].Type != common.ExpendGasOperation { + t.Error("Expected 1st operation to be gas") + } + if rosettaTx.Operations[1].Type != common.TransferNativeOperation { + t.Error("Expected 2nd operation to transfer related") + } + if rosettaTx.Operations[1].Metadata != nil { + t.Error("Expected 1st operation to have no metadata") + } + if rosettaTx.Operations[2].Metadata != nil { + t.Error("Expected 2nd operation to have no metadata") + } + if reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) { + t.Error("Expected transaction to have some metadata") + } + if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) { + t.Error("Expected sender to pay gas fee") + } +} + +func TestFormatGenesisTransaction(t *testing.T) { + genesisSpec := getGenesisSpec(0) + testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") + for acc := range genesisSpec.Alloc { + txID := getSpecialCaseTransactionIdentifier(testBlkHash, acc, SpecialGenesisTxID) + tx, rosettaError := FormatGenesisTransaction(txID, acc, 0) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(txID, tx.TransactionIdentifier) { + t.Error("expected transaction ID of formatted tx to be same as requested") + } + if len(tx.Operations) != 1 { + t.Error("expected exactly 1 operation") + } + if err := assertNativeOperationTypeUniquenessInvariant(tx.Operations); err != nil { + t.Error(err) + } + if tx.Operations[0].OperationIdentifier.Index != 0 { + t.Error("expected operational ID to be 0") + } + if tx.Operations[0].Type != common.GenesisFundsOperation { + t.Error("expected operation to be genesis funds operations") + } + if tx.Operations[0].Status != common.SuccessOperationStatus.Status { + t.Error("expected successful operation status") + } + } +} + +func TestFormatPreStakingRewardTransactionSuccess(t *testing.T) { + testKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + testAddr := crypto.PubkeyToAddress(testKey.PublicKey) + testBlockSigInfo := &hmy.DetailedBlockSignerInfo{ + Signers: map[ethcommon.Address][]bls.SerializedPublicKey{ + testAddr: { // Only care about length for this test + bls.SerializedPublicKey{}, + bls.SerializedPublicKey{}, + }, + }, + Committee: shard.SlotList{ + { + EcdsaAddress: testAddr, + }, + }, + TotalKeysSigned: 150, + BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), + } + refTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.BlockHash, testAddr, SpecialPreStakingRewardTxID) + tx, rosettaError := FormatPreStakingRewardTransaction(refTxID, testBlockSigInfo, testAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + if !reflect.DeepEqual(tx.TransactionIdentifier, refTxID) { + t.Errorf("Expected TxID %v got %v", refTxID, tx.TransactionIdentifier) + } + if len(tx.Operations) != 1 { + t.Fatal("Expected exactly 1 operation") + } + if err := assertNativeOperationTypeUniquenessInvariant(tx.Operations); err != nil { + t.Error(err) + } + if tx.Operations[0].OperationIdentifier.Index != 0 { + t.Error("expected operational ID to be 0") + } + 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") + } + + // Expect: myNumberOfSigForBlock * (totalAmountOfRewardsPerBlock / numOfSigsForBlock) to be my block reward amount + refAmount := new(big.Int).Mul(new(big.Int).Quo(stakingNetwork.BlockReward, big.NewInt(150)), big.NewInt(2)) + fmtRefAmount := fmt.Sprintf("%v", refAmount) + if tx.Operations[0].Amount.Value != fmtRefAmount { + t.Errorf("expected operation amount to be %v not %v", fmtRefAmount, tx.Operations[0].Amount.Value) + } + + testBlockSigInfo = &hmy.DetailedBlockSignerInfo{ + Signers: map[ethcommon.Address][]bls.SerializedPublicKey{ + testAddr: { // Only care about length for this test + bls.SerializedPublicKey{}, + bls.SerializedPublicKey{}, + }, + }, + Committee: shard.SlotList{}, + TotalKeysSigned: 150, + BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), + } + tx, rosettaError = FormatPreStakingRewardTransaction(refTxID, testBlockSigInfo, testAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if len(tx.Operations) != 1 { + t.Fatal("expected exactly 1 operation") + } + amt, err := types.AmountValue(tx.Operations[0].Amount) + if err != nil { + t.Fatal(err) + } + if amt.Cmp(big.NewInt(0)) != 0 { + t.Error("expected amount to be 0") + } +} + +func TestFormatPreStakingRewardTransactionFail(t *testing.T) { + testKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + testAddr := crypto.PubkeyToAddress(testKey.PublicKey) + testBlockSigInfo := &hmy.DetailedBlockSignerInfo{ + Signers: map[ethcommon.Address][]bls.SerializedPublicKey{ + testAddr: {}, + }, + Committee: shard.SlotList{ + { + EcdsaAddress: testAddr, + }, + }, + TotalKeysSigned: 150, + BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), + } + testTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.BlockHash, testAddr, SpecialPreStakingRewardTxID) + _, rosettaError := FormatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr) + if rosettaError == nil { + t.Fatal("expected rosetta error") + } + if !reflect.DeepEqual(&common.TransactionNotFoundError, rosettaError) { + t.Error("expected transaction not found error") + } + + testBlockSigInfo = &hmy.DetailedBlockSignerInfo{ + Signers: map[ethcommon.Address][]bls.SerializedPublicKey{}, + Committee: shard.SlotList{ + { + EcdsaAddress: testAddr, + }, + }, + TotalKeysSigned: 150, + BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"), + } + _, rosettaError = FormatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr) + if rosettaError == nil { + t.Fatal("expected rosetta error") + } + if !reflect.DeepEqual(&common.TransactionNotFoundError, rosettaError) { + t.Error("expected transaction not found error") + } +} + +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 err := assertNativeOperationTypeUniquenessInvariant(tx.Operations); err != nil { + t.Error(err) + } + 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, +) { + // Note that post EIP-155 epoch singer is tested in detailed tests. + signer := hmytypes.HomesteadSigner{} + tx, err := helpers.CreateTestTransaction( + signer, 0, 1, 0, 1e18, gasPrice, big.NewInt(1), []byte("test"), + ) + if err != nil { + t.Fatal(err.Error()) + } + senderAddr, err := tx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, + GasUsed: gasUsed, + } + rosettaTx, rosettaError := FormatTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if len(rosettaTx.Operations) != 2 { + t.Error("Expected 2 operations") + } + if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { + t.Error(err) + } + if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() { + t.Error("Invalid transaction") + } + if rosettaTx.Operations[0].Type != common.ExpendGasOperation { + t.Error("Expected 1st operation to be gas") + } + if rosettaTx.Operations[1].Type != common.CrossShardTransferNativeOperation { + t.Error("Expected 2nd operation to cross-shard transfer related") + } + if reflect.DeepEqual(rosettaTx.Operations[1].Metadata, map[string]interface{}{}) { + t.Error("Expected 1st operation to have metadata") + } + if reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) { + t.Error("Expected transaction to have some metadata") + } + if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) { + t.Error("Expected sender to pay gas fee") + } +} + +func TestFormatCrossShardReceiverTransaction(t *testing.T) { + signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) + tx, err := helpers.CreateTestTransaction( + signer, 0, 1, 0, 1e18, gasPrice, big.NewInt(1), []byte{}, + ) + if err != nil { + t.Fatal(err.Error()) + } + senderAddr, err := tx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + receiverAccID, rosettaError := newAccountIdentifier(*tx.To()) + if rosettaError != nil { + t.Fatal(rosettaError) + } + cxReceipt := &hmytypes.CXReceipt{ + TxHash: tx.Hash(), + From: senderAddr, + To: tx.To(), + ShardID: 0, + ToShardID: 1, + Amount: tx.Value(), + } + opMetadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ + From: senderAccID, + To: receiverAccID, + }) + if err != nil { + t.Error(err) + } + + refCxID := &types.TransactionIdentifier{Hash: tx.Hash().String()} + refOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, // There is no gas expenditure for cross-shard payout + }, + Type: common.CrossShardTransferNativeOperation, + Status: common.SuccessOperationStatus.Status, + Account: receiverAccID, + Amount: &types.Amount{ + Value: fmt.Sprintf("%v", tx.Value().Uint64()), + Currency: &common.NativeCurrency, + }, + Metadata: opMetadata, + }, + } + to := tx.ToShardID() + from := tx.ShardID() + refMetadata, err := types.MarshalMap(TransactionMetadata{ + CrossShardIdentifier: refCxID, + ToShardID: &to, + FromShardID: &from, + }) + refRosettaTx := &types.Transaction{ + TransactionIdentifier: refCxID, + Operations: refOperations, + Metadata: refMetadata, + } + rosettaTx, rosettaError := FormatCrossShardReceiverTransaction(cxReceipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(rosettaTx, refRosettaTx) { + t.Errorf("Expected transaction to be %v not %v", refRosettaTx, rosettaTx) + } + if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil { + t.Error(err) + } +} diff --git a/rosetta/services/tx_operation.go b/rosetta/services/tx_operation.go new file mode 100644 index 000000000..4d65d90d1 --- /dev/null +++ b/rosetta/services/tx_operation.go @@ -0,0 +1,385 @@ +package services + +import ( + "fmt" + "math/big" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + + hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/internal/utils" + "github.com/harmony-one/harmony/rosetta/common" + rpcV2 "github.com/harmony-one/harmony/rpc/v2" + "github.com/harmony-one/harmony/staking" + stakingTypes "github.com/harmony-one/harmony/staking/types" +) + +// GetNativeOperationsFromTransaction for one of the following transactions: +// contract creation, cross-shard sender, same-shard transfer. +// Native operations only include operations that affect the native currency balance of an account. +func GetNativeOperationsFromTransaction( + tx *hmytypes.Transaction, receipt *hmytypes.Receipt, +) ([]*types.Operation, *types.Error) { + senderAddress, err := tx.SenderAddress() + if err != nil { + senderAddress = FormatDefaultSenderAddress + } + accountID, rosettaError := newAccountIdentifier(senderAddress) + if rosettaError != nil { + return nil, rosettaError + } + + // All operations excepts for cross-shard tx payout expend gas + gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice()) + gasOperations := newNativeOperations(gasExpended, accountID) + + // Handle different cases of plain transactions + var txOperations []*types.Operation + if tx.To() == nil { + txOperations, rosettaError = newContractCreationNativeOperations( + gasOperations[0].OperationIdentifier, tx, receipt, senderAddress, + ) + } else if tx.ShardID() != tx.ToShardID() { + txOperations, rosettaError = newCrossShardSenderTransferNativeOperations( + gasOperations[0].OperationIdentifier, tx, senderAddress, + ) + } else { + txOperations, rosettaError = newTransferNativeOperations( + gasOperations[0].OperationIdentifier, tx, receipt, senderAddress, + ) + } + if rosettaError != nil { + return nil, rosettaError + } + + return append(gasOperations, txOperations...), nil +} + +// GetOperationsFromStakingTransaction for all staking directives +// Note that only native operations can come from staking transactions. +func GetOperationsFromStakingTransaction( + tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt, +) ([]*types.Operation, *types.Error) { + senderAddress, err := tx.SenderAddress() + if err != nil { + senderAddress = FormatDefaultSenderAddress + } + accountID, rosettaError := newAccountIdentifier(senderAddress) + if rosettaError != nil { + return nil, rosettaError + } + + // All operations excepts for cross-shard tx payout expend gas + gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice()) + gasOperations := newNativeOperations(gasExpended, accountID) + + // Format staking message for metadata using decimal numbers (hence usage of rpcV2) + rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + metadata, err := types.MarshalMap(rpcStakingTx.Msg) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + + // Set correct amount depending on staking message directive that apply balance changes INSTANTLY + var amount *types.Amount + switch tx.StakingType() { + case stakingTypes.DirectiveCreateValidator: + if amount, rosettaError = getAmountFromCreateValidatorMessage(tx.Data()); rosettaError != nil { + return nil, rosettaError + } + case stakingTypes.DirectiveDelegate: + if amount, rosettaError = getAmountFromDelegateMessage(receipt, tx.Data()); rosettaError != nil { + return nil, rosettaError + } + case stakingTypes.DirectiveCollectRewards: + if amount, rosettaError = getAmountFromCollectRewards(receipt, senderAddress); rosettaError != nil { + return nil, rosettaError + } + default: + amount = &types.Amount{ + Value: "0", // All other staking transactions do not apply balance changes instantly or at all + Currency: &common.NativeCurrency, + } + } + + return append(gasOperations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: gasOperations[0].OperationIdentifier.Index + 1, + }, + RelatedOperations: []*types.OperationIdentifier{ + gasOperations[0].OperationIdentifier, + }, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: accountID, + Amount: amount, + Metadata: metadata, + }), nil +} + +func getAmountFromCreateValidatorMessage(data []byte) (*types.Amount, *types.Error) { + msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveCreateValidator) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + stkMsg, ok := msg.(*stakingTypes.CreateValidator) + if !ok { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "unable to parse staking message for create validator tx", + }) + } + return &types.Amount{ + Value: negativeBigValue(stkMsg.Amount), + Currency: &common.NativeCurrency, + }, nil +} + +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{}{ + "message": err.Error(), + }) + } + stkMsg, ok := msg.(*stakingTypes.Delegate) + if !ok { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "unable to parse staking message for delegate tx", + }) + } + + stkAmount := stkMsg.Amount + logs := hmytypes.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: negativeBigValue(stkAmount), + Currency: &common.NativeCurrency, + }, nil +} + +func getAmountFromCollectRewards( + receipt *hmytypes.Receipt, senderAddress ethcommon.Address, +) (*types.Amount, *types.Error) { + var amount *types.Amount + logs := hmytypes.FindLogsWithTopic(receipt, staking.CollectRewardsTopic) + for _, log := range logs { + if log.Address == senderAddress { + amount = &types.Amount{ + Value: big.NewInt(0).SetBytes(log.Data).String(), + Currency: &common.NativeCurrency, + } + break + } + } + if amount == nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": fmt.Sprintf("collect rewards amount not found for %v", senderAddress), + }) + } + return amount, nil +} + +// newTransferNativeOperations extracts & formats the native operation(s) for plain transaction, +// including contract transactions. +func newTransferNativeOperations( + startingOperationID *types.OperationIdentifier, + tx *hmytypes.Transaction, receipt *hmytypes.Receipt, senderAddress ethcommon.Address, +) ([]*types.Operation, *types.Error) { + if tx.To() == nil { + return nil, common.NewError(common.CatchAllError, nil) + } + receiverAddress := *tx.To() + + // Common elements + opType := common.TransferNativeOperation + opStatus := common.SuccessOperationStatus.Status + if receipt.Status == hmytypes.ReceiptStatusFailed { + if len(tx.Data()) > 0 { + opStatus = common.ContractFailureOperationStatus.Status + } else { + // Should never see a failed non-contract related transaction on chain + opStatus = common.FailureOperationStatus.Status + utils.Logger().Warn().Msgf("Failed transaction on chain: %v", tx.Hash().String()) + } + } + + // Subtraction operation elements + subOperationID := &types.OperationIdentifier{ + Index: startingOperationID.Index + 1, + } + subRelatedID := []*types.OperationIdentifier{ + startingOperationID, + } + subAccountID, rosettaError := newAccountIdentifier(senderAddress) + if rosettaError != nil { + return nil, rosettaError + } + subAmount := &types.Amount{ + Value: negativeBigValue(tx.Value()), + Currency: &common.NativeCurrency, + } + + // Addition operation elements + addOperationID := &types.OperationIdentifier{ + Index: subOperationID.Index + 1, + } + addRelatedID := []*types.OperationIdentifier{ + subOperationID, + } + addAccountID, rosettaError := newAccountIdentifier(receiverAddress) + if rosettaError != nil { + return nil, rosettaError + } + addAmount := &types.Amount{ + Value: tx.Value().String(), + Currency: &common.NativeCurrency, + } + + return []*types.Operation{ + { + OperationIdentifier: subOperationID, + RelatedOperations: subRelatedID, + Type: opType, + Status: opStatus, + Account: subAccountID, + Amount: subAmount, + }, + { + OperationIdentifier: addOperationID, + RelatedOperations: addRelatedID, + Type: opType, + Status: opStatus, + Account: addAccountID, + Amount: addAmount, + }, + }, nil +} + +// newCrossShardSenderTransferNativeOperations extracts & formats the native operation(s) +// for cross-shard-tx on the sender's shard. +func newCrossShardSenderTransferNativeOperations( + startingOperationID *types.OperationIdentifier, + tx *hmytypes.Transaction, senderAddress ethcommon.Address, +) ([]*types.Operation, *types.Error) { + if tx.To() == nil { + return nil, common.NewError(common.CatchAllError, nil) + } + senderAccountID, rosettaError := newAccountIdentifier(senderAddress) + if rosettaError != nil { + return nil, rosettaError + } + receiverAccountID, rosettaError := newAccountIdentifier(*tx.To()) + if rosettaError != nil { + return nil, rosettaError + } + metadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ + From: senderAccountID, + To: receiverAccountID, + }) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + + return []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: startingOperationID.Index + 1, + }, + RelatedOperations: []*types.OperationIdentifier{ + startingOperationID, + }, + Type: common.CrossShardTransferNativeOperation, + Status: common.SuccessOperationStatus.Status, + Account: senderAccountID, + Amount: &types.Amount{ + Value: negativeBigValue(tx.Value()), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }, + }, nil +} + +// newContractCreationNativeOperations extracts & formats the native operation(s) for a contract creation tx +func newContractCreationNativeOperations( + startingOperationID *types.OperationIdentifier, + tx *hmytypes.Transaction, txReceipt *hmytypes.Receipt, senderAddress ethcommon.Address, +) ([]*types.Operation, *types.Error) { + senderAccountID, rosettaError := newAccountIdentifier(senderAddress) + if rosettaError != nil { + return nil, rosettaError + } + + // Set execution status as necessary + status := common.SuccessOperationStatus.Status + if txReceipt.Status == hmytypes.ReceiptStatusFailed { + status = common.ContractFailureOperationStatus.Status + } + contractAddressID, rosettaError := newAccountIdentifier(txReceipt.ContractAddress) + if rosettaError != nil { + return nil, rosettaError + } + + return []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: startingOperationID.Index + 1, + }, + RelatedOperations: []*types.OperationIdentifier{ + startingOperationID, + }, + Type: common.ContractCreationOperation, + Status: status, + Account: senderAccountID, + Amount: &types.Amount{ + Value: negativeBigValue(tx.Value()), + Currency: &common.NativeCurrency, + }, + Metadata: map[string]interface{}{ + "contract_address": contractAddressID, + }, + }, + }, nil +} + +// newNativeOperations creates a new operation with the gas fee as the first operation. +// Note: the gas fee is gasPrice * gasUsed. +func newNativeOperations( + gasFeeInATTO *big.Int, accountID *types.AccountIdentifier, +) []*types.Operation { + return []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, // gas operation is always first + }, + Type: common.ExpendGasOperation, + Status: common.SuccessOperationStatus.Status, + Account: accountID, + Amount: &types.Amount{ + Value: negativeBigValue(gasFeeInATTO), + Currency: &common.NativeCurrency, + }, + }, + } +} diff --git a/rosetta/services/operation_components.go b/rosetta/services/tx_operation_components.go similarity index 95% rename from rosetta/services/operation_components.go rename to rosetta/services/tx_operation_components.go index 532a3e635..405aaa899 100644 --- a/rosetta/services/operation_components.go +++ b/rosetta/services/tx_operation_components.go @@ -57,7 +57,7 @@ func GetOperationComponents( return getTransferOperationComponents(operations) } switch operations[0].Type { - case common.CrossShardTransferOperation: + case common.CrossShardTransferNativeOperation: return getCrossShardOperationComponents(operations[0]) case common.ContractCreationOperation: return getContractCreationOperationComponents(operations[0]) @@ -78,7 +78,7 @@ func getTransferOperationComponents( }) } op0, op1 := operations[0], operations[1] - if op0.Type != common.TransferOperation || op1.Type != common.TransferOperation { + if op0.Type != common.TransferNativeOperation || op1.Type != common.TransferNativeOperation { return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ "message": "invalid operation type(s) for same shard transfer", }) @@ -101,8 +101,8 @@ func getTransferOperationComponents( "message": "amount taken from sender is not exactly paid out to receiver for same shard transfer", }) } - if types.Hash(op0.Amount.Currency) != common.CurrencyHash || - types.Hash(op1.Amount.Currency) != common.CurrencyHash { + if types.Hash(op0.Amount.Currency) != common.NativeCurrencyHash || + types.Hash(op1.Amount.Currency) != common.NativeCurrencyHash { return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ "message": "invalid currency for provided amounts", }) @@ -170,7 +170,7 @@ func getCrossShardOperationComponents( "message": "sender amount must not be positive for cross shard transfer", }) } - if types.Hash(operation.Amount.Currency) != common.CurrencyHash { + if types.Hash(operation.Amount.Currency) != common.NativeCurrencyHash { return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ "message": "invalid currency for provided amounts", }) @@ -215,7 +215,7 @@ func getContractCreationOperationComponents( "message": "sender amount must not be positive for contract creation", }) } - if types.Hash(operation.Amount.Currency) != common.CurrencyHash { + if types.Hash(operation.Amount.Currency) != common.NativeCurrencyHash { return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ "message": "invalid currency for provided amounts", }) diff --git a/rosetta/services/operation_components_test.go b/rosetta/services/tx_operation_components_test.go similarity index 92% rename from rosetta/services/operation_components_test.go rename to rosetta/services/tx_operation_components_test.go index 3480adba5..dff96ad3f 100644 --- a/rosetta/services/operation_components_test.go +++ b/rosetta/services/tx_operation_components_test.go @@ -14,7 +14,7 @@ import ( func TestGetContractCreationOperationComponents(t *testing.T) { refAmount := &types.Amount{ Value: "-12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refKey := internalCommon.MustGeneratePrivateKey() refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refKey.PublicKey)) @@ -57,7 +57,7 @@ func TestGetContractCreationOperationComponents(t *testing.T) { Type: common.ContractCreationOperation, Amount: &types.Amount{ Value: "12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, }, Account: refFrom, }) @@ -101,7 +101,7 @@ func TestGetContractCreationOperationComponents(t *testing.T) { func TestGetCrossShardOperationComponents(t *testing.T) { refAmount := &types.Amount{ Value: "-12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refFromKey := internalCommon.MustGeneratePrivateKey() refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey)) @@ -124,7 +124,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) { // test valid operations refOperation := &types.Operation{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: refAmount, Account: refFrom, Metadata: refMetadataMap, @@ -148,7 +148,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) { // test nil amount _, rosettaError = getCrossShardOperationComponents(&types.Operation{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: nil, Account: refFrom, Metadata: refMetadataMap, @@ -159,10 +159,10 @@ func TestGetCrossShardOperationComponents(t *testing.T) { // test positive amount _, rosettaError = getCrossShardOperationComponents(&types.Operation{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: &types.Amount{ Value: "12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, }, Account: refFrom, Metadata: refMetadataMap, @@ -173,7 +173,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) { // test different/unsupported currency _, rosettaError = getCrossShardOperationComponents(&types.Operation{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: &types.Amount{ Value: "-12000", Currency: &types.Currency{ @@ -190,7 +190,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) { // test nil account _, rosettaError = getCrossShardOperationComponents(&types.Operation{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: refAmount, Account: nil, Metadata: refMetadataMap, @@ -201,7 +201,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) { // test no metadata _, rosettaError = getCrossShardOperationComponents(&types.Operation{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: refAmount, Account: refFrom, }) @@ -224,7 +224,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) { t.Fatal(err) } _, rosettaError = getCrossShardOperationComponents(&types.Operation{ - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: refAmount, Account: refFrom, Metadata: badMetadataMap, @@ -243,11 +243,11 @@ func TestGetCrossShardOperationComponents(t *testing.T) { func TestGetTransferOperationComponents(t *testing.T) { refFromAmount := &types.Amount{ Value: "-12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refToAmount := &types.Amount{ Value: "12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refFromKey := internalCommon.MustGeneratePrivateKey() refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey)) @@ -266,7 +266,7 @@ func TestGetTransferOperationComponents(t *testing.T) { OperationIdentifier: &types.OperationIdentifier{ Index: 0, }, - Type: common.TransferOperation, + Type: common.TransferNativeOperation, Amount: refFromAmount, Account: refFrom, }, @@ -279,7 +279,7 @@ func TestGetTransferOperationComponents(t *testing.T) { Index: 0, }, }, - Type: common.TransferOperation, + Type: common.TransferNativeOperation, Amount: refToAmount, Account: refTo, }, @@ -345,20 +345,20 @@ func TestGetTransferOperationComponents(t *testing.T) { // test invalid operation refOperations[0].Type = common.ExpendGasOperation - refOperations[1].Type = common.TransferOperation + refOperations[1].Type = common.TransferNativeOperation _, rosettaError = getTransferOperationComponents(refOperations) if rosettaError == nil { t.Error("expected error") } // test invalid operation sender - refOperations[0].Type = common.TransferOperation + refOperations[0].Type = common.TransferNativeOperation refOperations[1].Type = common.ExpendGasOperation _, rosettaError = getTransferOperationComponents(refOperations) if rosettaError == nil { t.Error("expected error") } - refOperations[1].Type = common.TransferOperation + refOperations[1].Type = common.TransferNativeOperation // test nil amount refOperations[0].Amount = nil @@ -385,7 +385,7 @@ func TestGetTransferOperationComponents(t *testing.T) { refOperations[0].Account = refFrom refOperations[1].Amount = &types.Amount{ Value: "0", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refOperations[1].Account = refTo _, rosettaError = getTransferOperationComponents(refOperations) @@ -396,7 +396,7 @@ func TestGetTransferOperationComponents(t *testing.T) { // test uneven amount sender refOperations[0].Amount = &types.Amount{ Value: "0", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refOperations[0].Account = refFrom refOperations[1].Amount = refToAmount @@ -439,7 +439,7 @@ func TestGetTransferOperationComponents(t *testing.T) { if rosettaError == nil { t.Error("expected error") } - refOperations[0].Amount.Currency = &common.Currency + refOperations[0].Amount.Currency = &common.NativeCurrency // test invalid currency sender refOperations[0].Amount = refFromAmount @@ -454,7 +454,7 @@ func TestGetTransferOperationComponents(t *testing.T) { if rosettaError == nil { t.Error("expected error") } - refOperations[1].Amount.Currency = &common.Currency + refOperations[1].Amount.Currency = &common.NativeCurrency // test invalid related operation refOperations[1].RelatedOperations[0].Index = 2 @@ -493,11 +493,11 @@ func TestGetTransferOperationComponents(t *testing.T) { func TestGetOperationComponents(t *testing.T) { refFromAmount := &types.Amount{ Value: "-12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refToAmount := &types.Amount{ Value: "12000", - Currency: &common.Currency, + Currency: &common.NativeCurrency, } refFromKey := internalCommon.MustGeneratePrivateKey() refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey)) @@ -517,7 +517,7 @@ func TestGetOperationComponents(t *testing.T) { OperationIdentifier: &types.OperationIdentifier{ Index: 0, }, - Type: common.TransferOperation, + Type: common.TransferNativeOperation, Amount: refFromAmount, Account: refFrom, }, @@ -530,7 +530,7 @@ func TestGetOperationComponents(t *testing.T) { Index: 0, }, }, - Type: common.TransferOperation, + Type: common.TransferNativeOperation, Amount: refToAmount, Account: refTo, }, @@ -551,7 +551,7 @@ func TestGetOperationComponents(t *testing.T) { } _, rosettaError = GetOperationComponents([]*types.Operation{ { - Type: common.CrossShardTransferOperation, + Type: common.CrossShardTransferNativeOperation, Amount: refFromAmount, Account: refFrom, Metadata: refMetadataMap, diff --git a/rosetta/services/tx_operation_test.go b/rosetta/services/tx_operation_test.go new file mode 100644 index 000000000..854ce5ce7 --- /dev/null +++ b/rosetta/services/tx_operation_test.go @@ -0,0 +1,594 @@ +package services + +import ( + "fmt" + "math/big" + "reflect" + "testing" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/internal/params" + "github.com/harmony-one/harmony/rosetta/common" + "github.com/harmony-one/harmony/staking" + stakingTypes "github.com/harmony-one/harmony/staking/types" + "github.com/harmony-one/harmony/test/helpers" +) + +func TestGetStakingOperationsFromCreateValidator(t *testing.T) { + gasLimit := uint64(1e18) + createValidatorTxDescription := stakingTypes.Description{ + Name: "SuperHero", + Identity: "YouWouldNotKnow", + Website: "Secret Website", + SecurityContact: "LicenseToKill", + Details: "blah blah blah", + } + tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { + fromKey, _ := crypto.GenerateKey() + return stakingTypes.DirectiveCreateValidator, stakingTypes.CreateValidator{ + Description: createValidatorTxDescription, + MinSelfDelegation: tenOnes, + MaxTotalDelegation: twelveOnes, + ValidatorAddress: crypto.PubkeyToAddress(fromKey.PublicKey), + Amount: tenOnes, + } + }, nil, 0, gasLimit, gasPrice) + if err != nil { + t.Fatal(err.Error()) + } + metadata, err := helpers.GetMessageFromStakingTx(tx) + if err != nil { + t.Fatal(err.Error()) + } + senderAddr, err := tx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + gasUsed := uint64(1e5) + gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain + GasUsed: gasUsed, + } + refOperations := newNativeOperations(gasFee, senderAccID) + refOperations = append(refOperations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: negativeBigValue(tenOnes), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }) + operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestGetStakingOperationsFromDelegate(t *testing.T) { + gasLimit := uint64(1e18) + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) + validatorKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey) + tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{ + DelegatorAddress: senderAddr, + ValidatorAddress: validatorAddr, + Amount: tenOnes, + } + }, senderKey, 0, gasLimit, gasPrice) + if err != nil { + t.Fatal(err.Error()) + } + metadata, err := helpers.GetMessageFromStakingTx(tx) + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + gasUsed := uint64(1e5) + gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain + GasUsed: gasUsed, + } + refOperations := newNativeOperations(gasFee, senderAccID) + refOperations = append(refOperations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: negativeBigValue(tenOnes), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }) + operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestGetStakingOperationsFromUndelegate(t *testing.T) { + gasLimit := uint64(1e18) + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) + validatorKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey) + tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveUndelegate, stakingTypes.Undelegate{ + DelegatorAddress: senderAddr, + ValidatorAddress: validatorAddr, + Amount: tenOnes, + } + }, senderKey, 0, gasLimit, gasPrice) + if err != nil { + t.Fatal(err.Error()) + } + metadata, err := helpers.GetMessageFromStakingTx(tx) + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + gasUsed := uint64(1e5) + gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain + GasUsed: gasUsed, + } + refOperations := newNativeOperations(gasFee, senderAccID) + refOperations = append(refOperations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: fmt.Sprintf("0"), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }) + operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestGetStakingOperationsFromCollectRewards(t *testing.T) { + gasLimit := uint64(1e18) + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) + tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveCollectRewards, stakingTypes.CollectRewards{ + DelegatorAddress: senderAddr, + } + }, senderKey, 0, gasLimit, gasPrice) + if err != nil { + t.Fatal(err.Error()) + } + metadata, err := helpers.GetMessageFromStakingTx(tx) + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + gasUsed := uint64(1e5) + gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain + GasUsed: gasUsed, + Logs: []*hmytypes.Log{ + { + Address: senderAddr, + Topics: []ethcommon.Hash{staking.CollectRewardsTopic}, + Data: tenOnes.Bytes(), + }, + }, + } + refOperations := newNativeOperations(gasFee, senderAccID) + refOperations = append(refOperations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: fmt.Sprintf("%v", tenOnes.Uint64()), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }) + operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestGetStakingOperationsFromEditValidator(t *testing.T) { + gasLimit := uint64(1e18) + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) + tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveEditValidator, stakingTypes.EditValidator{ + ValidatorAddress: senderAddr, + } + }, senderKey, 0, gasLimit, gasPrice) + if err != nil { + t.Fatal(err.Error()) + } + metadata, err := helpers.GetMessageFromStakingTx(tx) + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + gasUsed := uint64(1e5) + gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed))) + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain + GasUsed: gasUsed, + } + refOperations := newNativeOperations(gasFee, senderAccID) + refOperations = append(refOperations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: fmt.Sprintf("0"), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }) + operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestNewTransferNativeOperations(t *testing.T) { + signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) + tx, err := helpers.CreateTestTransaction( + signer, 0, 0, 0, 1e18, gasPrice, big.NewInt(1), []byte("test"), + ) + if err != nil { + t.Fatal(err.Error()) + } + senderAddr, err := tx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + receiverAccID, rosettaError := newAccountIdentifier(*tx.To()) + if rosettaError != nil { + t.Fatal(rosettaError) + } + startingOpID := &types.OperationIdentifier{} + + // Test failed 'contract' transaction + refOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: startingOpID.Index + 1, + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: startingOpID.Index, + }, + }, + Type: common.TransferNativeOperation, + Status: common.ContractFailureOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: negativeBigValue(tx.Value()), + Currency: &common.NativeCurrency, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: startingOpID.Index + 2, + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: startingOpID.Index + 1, + }, + }, + Type: common.TransferNativeOperation, + Status: common.ContractFailureOperationStatus.Status, + Account: receiverAccID, + Amount: &types.Amount{ + Value: fmt.Sprintf("%v", tx.Value().Uint64()), + Currency: &common.NativeCurrency, + }, + }, + } + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusFailed, + } + operations, rosettaError := newTransferNativeOperations(startingOpID, tx, receipt, senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } + + // Test successful plain / contract transaction + refOperations[0].Status = common.SuccessOperationStatus.Status + refOperations[1].Status = common.SuccessOperationStatus.Status + receipt.Status = hmytypes.ReceiptStatusSuccessful + operations, rosettaError = newTransferNativeOperations(startingOpID, tx, receipt, senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestNewCrossShardSenderTransferNativeOperations(t *testing.T) { + signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) + tx, err := helpers.CreateTestTransaction( + signer, 0, 1, 0, 1e18, gasPrice, big.NewInt(1), []byte("data-does-nothing"), + ) + if err != nil { + t.Fatal(err.Error()) + } + senderAddr, err := tx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + startingOpID := &types.OperationIdentifier{} + receiverAccID, rosettaError := newAccountIdentifier(*tx.To()) + if rosettaError != nil { + t.Error(rosettaError) + } + metadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{ + From: senderAccID, + To: receiverAccID, + }) + if err != nil { + t.Fatal(err) + } + + refOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: startingOpID.Index + 1, + }, + RelatedOperations: []*types.OperationIdentifier{ + startingOpID, + }, + Type: common.CrossShardTransferNativeOperation, + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: negativeBigValue(tx.Value()), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }, + } + operations, rosettaError := newCrossShardSenderTransferNativeOperations(startingOpID, tx, senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestNewContractCreationNativeOperations(t *testing.T) { + dummyContractKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf(err.Error()) + } + chainID := params.TestChainConfig.ChainID + signer := hmytypes.NewEIP155Signer(chainID) + tx, err := helpers.CreateTestContractCreationTransaction( + signer, 0, 0, 1e18, gasPrice, big.NewInt(0), []byte("test"), + ) + if err != nil { + t.Fatal(err.Error()) + } + senderAddr, err := tx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + senderAccID, rosettaError := newAccountIdentifier(senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + startingOpID := &types.OperationIdentifier{} + + // Test failed contract creation + contractAddr := crypto.PubkeyToAddress(dummyContractKey.PublicKey) + contractAddressID, rosettaError := newAccountIdentifier(contractAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + refOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: startingOpID.Index + 1, + }, + RelatedOperations: []*types.OperationIdentifier{ + startingOpID, + }, + Type: common.ContractCreationOperation, + Status: common.ContractFailureOperationStatus.Status, + Account: senderAccID, + Amount: &types.Amount{ + Value: negativeBigValue(tx.Value()), + Currency: &common.NativeCurrency, + }, + Metadata: map[string]interface{}{ + "contract_address": contractAddressID, + }, + }, + } + receipt := &hmytypes.Receipt{ + Status: hmytypes.ReceiptStatusFailed, + ContractAddress: contractAddr, + } + operations, rosettaError := newContractCreationNativeOperations(startingOpID, tx, receipt, senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } + + // Test successful contract creation + refOperations[0].Status = common.SuccessOperationStatus.Status + receipt.Status = hmytypes.ReceiptStatusSuccessful // Indicate successful tx + operations, rosettaError = newContractCreationNativeOperations(startingOpID, tx, receipt, senderAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if !reflect.DeepEqual(operations, refOperations) { + t.Errorf("Expected operations to be %v not %v", refOperations, operations) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } +} + +func TestNewNativeOperations(t *testing.T) { + accountID := &types.AccountIdentifier{ + Address: "test-address", + } + gasFee := big.NewInt(int64(1e18)) + amount := &types.Amount{ + Value: negativeBigValue(gasFee), + Currency: &common.NativeCurrency, + } + + ops := newNativeOperations(gasFee, accountID) + if len(ops) != 1 { + t.Fatalf("Expected new operations to be of length 1") + } + if !reflect.DeepEqual(ops[0].Account, accountID) { + t.Errorf("Expected account ID to be %v not %v", accountID, ops[0].OperationIdentifier) + } + if !reflect.DeepEqual(ops[0].Amount, amount) { + t.Errorf("Expected amount to be %v not %v", amount, ops[0].Amount) + } + if ops[0].Type != common.ExpendGasOperation { + t.Errorf("Expected operation to be %v not %v", common.ExpendGasOperation, ops[0].Type) + } + if ops[0].OperationIdentifier.Index != 0 { + t.Errorf("Expected operational ID to be of index 0") + } + if ops[0].Status != common.SuccessOperationStatus.Status { + t.Errorf("Expected operation status to be %v", common.SuccessOperationStatus.Status) + } +} diff --git a/test/helpers/transaction.go b/test/helpers/transaction.go new file mode 100644 index 000000000..835f3ccb9 --- /dev/null +++ b/test/helpers/transaction.go @@ -0,0 +1,81 @@ +package helpers + +import ( + "crypto/ecdsa" + "math/big" + + "github.com/coinbase/rosetta-sdk-go/types" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + hmytypes "github.com/harmony-one/harmony/core/types" + rpcV2 "github.com/harmony-one/harmony/rpc/v2" + stakingTypes "github.com/harmony-one/harmony/staking/types" +) + +// CreateTestStakingTransaction creates a pre-signed staking transaction +func CreateTestStakingTransaction( + payloadMaker func() (stakingTypes.Directive, interface{}), key *ecdsa.PrivateKey, + nonce, gasLimit uint64, gasPrice *big.Int, +) (*stakingTypes.StakingTransaction, error) { + tx, err := stakingTypes.NewStakingTransaction(nonce, gasLimit, gasPrice, payloadMaker) + if err != nil { + return nil, err + } + if key == nil { + key, err = crypto.GenerateKey() + if err != nil { + return nil, err + } + } + // Staking transactions are always post EIP155 epoch + return stakingTypes.Sign(tx, stakingTypes.NewEIP155Signer(tx.ChainID()), key) +} + +// GetMessageFromStakingTx gets the staking message, as seen by the rpc layer +func GetMessageFromStakingTx(tx *stakingTypes.StakingTransaction) (map[string]interface{}, error) { + rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0) + if err != nil { + return nil, err + } + return types.MarshalMap(rpcStakingTx.Msg) +} + +// CreateTestTransaction creates a pre-signed transaction +func CreateTestTransaction( + signer hmytypes.Signer, fromShard, toShard uint32, nonce, gasLimit uint64, + gasPrice, amount *big.Int, data []byte, +) (*hmytypes.Transaction, error) { + fromKey, err := crypto.GenerateKey() + if err != nil { + return nil, err + } + toKey, err := crypto.GenerateKey() + if err != nil { + return nil, err + } + toAddr := crypto.PubkeyToAddress(toKey.PublicKey) + var tx *hmytypes.Transaction + if fromShard != toShard { + tx = hmytypes.NewCrossShardTransaction( + nonce, &toAddr, fromShard, toShard, amount, gasLimit, gasPrice, data, + ) + } else { + tx = hmytypes.NewTransaction( + nonce, toAddr, fromShard, amount, gasLimit, gasPrice, data, + ) + } + return hmytypes.SignTx(tx, signer, fromKey) +} + +// CreateTestContractCreationTransaction creates a pre-signed contract creation transaction +func CreateTestContractCreationTransaction( + signer hmytypes.Signer, shard uint32, nonce, gasLimit uint64, gasPrice, amount *big.Int, data []byte, +) (*hmytypes.Transaction, error) { + fromKey, err := crypto.GenerateKey() + if err != nil { + return nil, err + } + tx := hmytypes.NewContractCreation(nonce, shard, amount, gasLimit, gasPrice, data) + return hmytypes.SignTx(tx, signer, fromKey) +}