From 6b143bb048a5b4cee0566c63861a568cf25bf11b Mon Sep 17 00:00:00 2001 From: Daniel Van Der Maden Date: Thu, 3 Dec 2020 11:43:20 -0800 Subject: [PATCH] [Rosetta] Track internal transactions (#3475) * [rosetta] Refactor operations & prep for internal tx exposure * Remove gas op relation for tx operations. Gas is for submission & processing the tx, thus not really related to the amount being transferred * Make optional starting op a ptr to a uint to keep consistent * Reorg file for consistency of fn placement * Rename functions for clarity * Make getContractCreationNativeOperations consume getBasicTransferOperations for consistency * Remove invariant doc as it does not apply anymore Signed-off-by: Daniel Van Der Maden * [rosetta] Add framework for parsing traced txs * Define ContractInfo struct for FormatTransaction * Add tx trace helper function defs & propagate type defs * Add a GetTransactionStatus helper fn for Operation formatting/creation * Add wrapper function, getContractTransferNativeOperations, to get internal operations Signed-off-by: Daniel Van Der Maden * [rosetta] Implement transaction tracer Signed-off-by: Daniel Van Der Maden * [tracer] Add CallerAddress & CodeAddress to tracer logs Signed-off-by: Daniel Van Der Maden * [tracer] Remove ptr to slice & map in StructLogRes Signed-off-by: Daniel Van Der Maden * [rosetta] Implement getContractInternalTransferNativeOperations * Add ContractAddress to ContractInfo for future usages (i.e: erc20 parsing) * Only check for contract address if there is tx data in BlockTransaction Signed-off-by: Daniel Van Der Maden * [rosetta] Fix status report for contract related txs Signed-off-by: Daniel Van Der Maden * [tracer] Expose contract address instead of code address Signed-off-by: Daniel Van Der Maden * [rosetta] Add trace cache & update TODO * Trace any PLAIN transaction instead of only transactions with data. This is to account for fall back contract fn calls & ignore staking data. Signed-off-by: Daniel Van Der Maden * [rosetta] Make internal tx formatter not return err on nil exec result Signed-off-by: Daniel Van Der Maden * Fix lint Signed-off-by: Daniel Van Der Maden * [rosetta] Fix tests Signed-off-by: Daniel Van Der Maden * [rosetta] Add internal tx unit tests * Fix tx data len check for contract related transfers as a transaction with len 0 data can happen for a contract (and fail) due to fall back operations. Signed-off-by: Daniel Van Der Maden * [rosetta] Update invariant comment Signed-off-by: Daniel Van Der Maden * [rosetta] Expose mutually exclusive ops + update docs & tests Signed-off-by: Daniel Van Der Maden * [rosetta] Fix docs and err msgs Signed-off-by: Daniel Van Der Maden --- core/vm/gen_structlog.go | 60 ++- core/vm/logger.go | 26 +- core/vm/logger_json.go | 20 +- hmy/tracer.go | 40 +- rosetta/common/operations.go | 13 +- rosetta/rosetta.go | 2 +- rosetta/services/block.go | 94 +++- rosetta/services/construction_parse.go | 8 +- rosetta/services/construction_parse_test.go | 4 +- rosetta/services/mempool.go | 2 +- rosetta/services/tx_format.go | 16 +- rosetta/services/tx_format_test.go | 9 +- rosetta/services/tx_operation.go | 408 +++++++++------- rosetta/services/tx_operation_test.go | 497 ++++++++++++++++++-- 14 files changed, 892 insertions(+), 307 deletions(-) diff --git a/core/vm/gen_structlog.go b/core/vm/gen_structlog.go index 726012e59..fb176a569 100644 --- a/core/vm/gen_structlog.go +++ b/core/vm/gen_structlog.go @@ -16,23 +16,27 @@ var _ = (*structLogMarshaling)(nil) // MarshalJSON marshals as JSON. func (s StructLog) MarshalJSON() ([]byte, error) { type StructLog struct { - Pc uint64 `json:"pc"` - Op OpCode `json:"op"` - Gas math.HexOrDecimal64 `json:"gas"` - GasCost math.HexOrDecimal64 `json:"gasCost"` - Memory hexutil.Bytes `json:"memory"` - MemorySize int `json:"memSize"` - Stack []*math.HexOrDecimal256 `json:"stack"` - Storage map[common.Hash]common.Hash `json:"-"` - Depth int `json:"depth"` - RefundCounter uint64 `json:"refund"` - Err error `json:"-"` - OpName string `json:"opName"` - ErrorString string `json:"error"` + Pc uint64 `json:"pc"` + Op OpCode `json:"op"` + CallerAddress common.Address `json:"callerAddress"` + ContractAddress common.Address `json:"contractAddress"` + Gas math.HexOrDecimal64 `json:"gas"` + GasCost math.HexOrDecimal64 `json:"gasCost"` + Memory hexutil.Bytes `json:"memory"` + MemorySize int `json:"memSize"` + Stack []*math.HexOrDecimal256 `json:"stack"` + Storage map[common.Hash]common.Hash `json:"-"` + Depth int `json:"depth"` + RefundCounter uint64 `json:"refund"` + Err error `json:"-"` + OpName string `json:"opName"` + ErrorString string `json:"error"` } var enc StructLog enc.Pc = s.Pc enc.Op = s.Op + enc.CallerAddress = s.CallerAddress + enc.ContractAddress = s.ContractAddress enc.Gas = math.HexOrDecimal64(s.Gas) enc.GasCost = math.HexOrDecimal64(s.GasCost) enc.Memory = s.Memory @@ -55,17 +59,19 @@ func (s StructLog) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (s *StructLog) UnmarshalJSON(input []byte) error { type StructLog struct { - Pc *uint64 `json:"pc"` - Op *OpCode `json:"op"` - Gas *math.HexOrDecimal64 `json:"gas"` - GasCost *math.HexOrDecimal64 `json:"gasCost"` - Memory *hexutil.Bytes `json:"memory"` - MemorySize *int `json:"memSize"` - Stack []*math.HexOrDecimal256 `json:"stack"` - Storage map[common.Hash]common.Hash `json:"-"` - Depth *int `json:"depth"` - RefundCounter *uint64 `json:"refund"` - Err error `json:"-"` + Pc *uint64 `json:"pc"` + Op *OpCode `json:"op"` + CallerAddress *common.Address `json:"callerAddress"` + ContractAddress *common.Address `json:"contractAddress"` + Gas *math.HexOrDecimal64 `json:"gas"` + GasCost *math.HexOrDecimal64 `json:"gasCost"` + Memory *hexutil.Bytes `json:"memory"` + MemorySize *int `json:"memSize"` + Stack []*math.HexOrDecimal256 `json:"stack"` + Storage map[common.Hash]common.Hash `json:"-"` + Depth *int `json:"depth"` + RefundCounter *uint64 `json:"refund"` + Err error `json:"-"` } var dec StructLog if err := json.Unmarshal(input, &dec); err != nil { @@ -77,6 +83,12 @@ func (s *StructLog) UnmarshalJSON(input []byte) error { if dec.Op != nil { s.Op = *dec.Op } + if dec.CallerAddress != nil { + s.CallerAddress = *dec.CallerAddress + } + if dec.ContractAddress != nil { + s.ContractAddress = *dec.ContractAddress + } if dec.Gas != nil { s.Gas = uint64(*dec.Gas) } diff --git a/core/vm/logger.go b/core/vm/logger.go index 33c58ac9d..215ae4484 100644 --- a/core/vm/logger.go +++ b/core/vm/logger.go @@ -56,17 +56,19 @@ type LogConfig struct { // StructLog is emitted to the EVM each cycle and lists information about the current internal state // prior to the execution of the statement. type StructLog struct { - Pc uint64 `json:"pc"` - Op OpCode `json:"op"` - Gas uint64 `json:"gas"` - GasCost uint64 `json:"gasCost"` - Memory []byte `json:"memory"` - MemorySize int `json:"memSize"` - Stack []*big.Int `json:"stack"` - Storage map[common.Hash]common.Hash `json:"-"` - Depth int `json:"depth"` - RefundCounter uint64 `json:"refund"` - Err error `json:"-"` + Pc uint64 `json:"pc"` + Op OpCode `json:"op"` + CallerAddress common.Address `json:"callerAddress"` + ContractAddress common.Address `json:"contractAddress"` + Gas uint64 `json:"gas"` + GasCost uint64 `json:"gasCost"` + Memory []byte `json:"memory"` + MemorySize int `json:"memSize"` + Stack []*big.Int `json:"stack"` + Storage map[common.Hash]common.Hash `json:"-"` + Depth int `json:"depth"` + RefundCounter uint64 `json:"refund"` + Err error `json:"-"` } // overrides for gencodec @@ -178,7 +180,7 @@ func (l *StructLogger) CaptureState(env *EVM, pc uint64, op OpCode, gas, cost ui storage = l.changedValues[contract.Address()].Copy() } // create a new snapshot of the EVM. - log := StructLog{pc, op, gas, cost, mem, memory.Len(), stck, storage, depth, env.StateDB.GetRefund(), err} + log := StructLog{pc, op, contract.CallerAddress, contract.Address(), gas, cost, mem, memory.Len(), stck, storage, depth, env.StateDB.GetRefund(), err} l.logs = append(l.logs, log) return nil diff --git a/core/vm/logger_json.go b/core/vm/logger_json.go index 7dcfe6179..4bb54f5f1 100644 --- a/core/vm/logger_json.go +++ b/core/vm/logger_json.go @@ -50,15 +50,17 @@ func (l *JSONLogger) CaptureStart(from common.Address, to common.Address, create // CaptureState outputs state information on the logger. func (l *JSONLogger) CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, memory *Memory, stack *Stack, contract *Contract, depth int, err error) error { log := StructLog{ - Pc: pc, - Op: op, - Gas: gas, - GasCost: cost, - MemorySize: memory.Len(), - Storage: nil, - Depth: depth, - RefundCounter: env.StateDB.GetRefund(), - Err: err, + Pc: pc, + Op: op, + ContractAddress: contract.Address(), + CallerAddress: contract.CallerAddress, + Gas: gas, + GasCost: cost, + MemorySize: memory.Len(), + Storage: nil, + Depth: depth, + RefundCounter: env.StateDB.GetRefund(), + Err: err, } if !l.cfg.DisableMemory { log.Memory = memory.Data() diff --git a/hmy/tracer.go b/hmy/tracer.go index f63b3e291..0265f0e36 100644 --- a/hmy/tracer.go +++ b/hmy/tracer.go @@ -719,15 +719,17 @@ type ExecutionResult struct { // StructLogRes stores a structured log emitted by the EVM while replaying a // transaction in debug mode type StructLogRes struct { - Pc uint64 `json:"pc"` - Op string `json:"op"` - Gas uint64 `json:"gas"` - GasCost uint64 `json:"gasCost"` - Depth int `json:"depth"` - Error error `json:"error,omitempty"` - Stack *[]string `json:"stack,omitempty"` - Memory *[]string `jsogun:"memory,omitempty"` - Storage *map[string]string `json:"storage,omitempty"` + Pc uint64 `json:"pc"` + Op string `json:"op"` + CallerAddress common.Address `json:"callerAddress"` + ContractAddress common.Address `json:"contractAddress"` + Gas uint64 `json:"gas"` + GasCost uint64 `json:"gasCost"` + Depth int `json:"depth"` + Error error `json:"error,omitempty"` + Stack []string `json:"stack,omitempty"` + Memory []string `json:"memory,omitempty"` + Storage map[string]string `json:"storage,omitempty"` } // FormatLogs formats EVM returned structured logs for json output @@ -735,33 +737,35 @@ func FormatLogs(logs []vm.StructLog) []StructLogRes { formatted := make([]StructLogRes, len(logs)) for index, trace := range logs { formatted[index] = StructLogRes{ - Pc: trace.Pc, - Op: trace.Op.String(), - Gas: trace.Gas, - GasCost: trace.GasCost, - Depth: trace.Depth, - Error: trace.Err, + Pc: trace.Pc, + Op: trace.Op.String(), + CallerAddress: trace.CallerAddress, + ContractAddress: trace.ContractAddress, + Gas: trace.Gas, + GasCost: trace.GasCost, + Depth: trace.Depth, + Error: trace.Err, } if trace.Stack != nil { stack := make([]string, len(trace.Stack)) for i, stackValue := range trace.Stack { stack[i] = fmt.Sprintf("%x", math.PaddedBigBytes(stackValue, 32)) } - formatted[index].Stack = &stack + formatted[index].Stack = stack } if trace.Memory != nil { memory := make([]string, 0, (len(trace.Memory)+31)/32) for i := 0; i+32 <= len(trace.Memory); i += 32 { memory = append(memory, fmt.Sprintf("%x", trace.Memory[i:i+32])) } - formatted[index].Memory = &memory + formatted[index].Memory = memory } if trace.Storage != nil { storage := make(map[string]string) for i, storageValue := range trace.Storage { storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue) } - formatted[index].Storage = &storage + formatted[index].Storage = storage } } return formatted diff --git a/rosetta/common/operations.go b/rosetta/common/operations.go index 7135ef169..969b0e84d 100644 --- a/rosetta/common/operations.go +++ b/rosetta/common/operations.go @@ -10,20 +10,19 @@ import ( staking "github.com/harmony-one/harmony/staking/types" ) -// Invariant: A transaction can only contain 1 type of native operation(s) other than gas expenditure. const ( // ExpendGasOperation is an operation that only affects the native currency. ExpendGasOperation = "Gas" + // ContractCreationOperation is an operation that only affects the native currency. + ContractCreationOperation = "ContractCreation" + // NativeTransferOperation is an operation that only affects the native currency. NativeTransferOperation = "NativeTransfer" // NativeCrossShardTransferOperation is an operation that only affects the native currency. NativeCrossShardTransferOperation = "NativeCrossShardTransfer" - // ContractCreationOperation is an operation that only affects the native currency. - ContractCreationOperation = "ContractCreation" - // GenesisFundsOperation is a side effect operation for genesis block only. // Note that no transaction can be constructed with this operation. GenesisFundsOperation = "Genesis" @@ -57,6 +56,12 @@ var ( staking.DirectiveUndelegate.String(), staking.DirectiveCollectRewards.String(), } + + // MutuallyExclusiveOperations for invariant: A transaction can only contain 1 type of 'native' operation. + MutuallyExclusiveOperations = map[string]interface{}{ + NativeTransferOperation: struct{}{}, + NativeCrossShardTransferOperation: struct{}{}, + } ) var ( diff --git a/rosetta/rosetta.go b/rosetta/rosetta.go index db830fe5f..e22130969 100644 --- a/rosetta/rosetta.go +++ b/rosetta/rosetta.go @@ -23,7 +23,7 @@ import ( var listener net.Listener // StartServers starts the rosetta http server -// TODO (dm): optimize rosetta to use single flight to avoid re-processing data +// TODO (dm): optimize rosetta to use single flight & use extra caching type DB to avoid re-processing data func StartServers(hmy *hmy.Harmony, config nodeconfig.RosettaServerConfig) error { if !config.HTTPEnabled { utils.Logger().Info().Msg("Rosetta http server disabled...") diff --git a/rosetta/services/block.go b/rosetta/services/block.go index 0c54f5559..ecc607062 100644 --- a/rosetta/services/block.go +++ b/rosetta/services/block.go @@ -4,28 +4,39 @@ import ( "context" "fmt" "math/big" + "time" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" ethcommon "github.com/ethereum/go-ethereum/common" + lru "github.com/hashicorp/golang-lru" "github.com/harmony-one/harmony/core/rawdb" hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/hmy" "github.com/harmony-one/harmony/rosetta/common" "github.com/harmony-one/harmony/rpc" stakingTypes "github.com/harmony-one/harmony/staking/types" ) +const ( + // txTraceCacheSize is max number of transaction traces to keep cached + txTraceCacheSize = 1e5 +) + // BlockAPI implements the server.BlockAPIServicer interface. type BlockAPI struct { - hmy *hmy.Harmony + hmy *hmy.Harmony + txTraceCache *lru.Cache } // NewBlockAPI creates a new instance of a BlockAPI. func NewBlockAPI(hmy *hmy.Harmony) server.BlockAPIServicer { + traceCache, _ := lru.New(txTraceCacheSize) return &BlockAPI{ - hmy: hmy, + hmy: hmy, + txTraceCache: traceCache, } } @@ -150,11 +161,30 @@ func (s *BlockAPI) BlockTransaction( var transaction *types.Transaction if txInfo.tx != nil && txInfo.receipt != nil { - contractCode := []byte{} - if txInfo.tx.To() != nil { - contractCode = state.GetCode(*txInfo.tx.To()) + contractInfo := &ContractInfo{} + if _, ok := txInfo.tx.(*hmytypes.Transaction); ok { + // check for contract related operations, if it is a plain transaction. + if txInfo.tx.To() != nil { + // possible call to existing contract so fetch relevant data + contractInfo.ContractCode = state.GetCode(*txInfo.tx.To()) + contractInfo.ContractAddress = txInfo.tx.To() + } else { + // contract creation, so address is in receipt + contractInfo.ContractCode = state.GetCode(txInfo.receipt.ContractAddress) + contractInfo.ContractAddress = &txInfo.receipt.ContractAddress + } + blk, rosettaError := getBlock( + ctx, s.hmy, &types.PartialBlockIdentifier{Hash: &request.BlockIdentifier.Hash}, + ) + if rosettaError != nil { + return nil, rosettaError + } + contractInfo.ExecutionResult, rosettaError = s.getTransactionTrace(ctx, blk, txInfo) + if rosettaError != nil { + return nil, rosettaError + } } - transaction, rosettaError = FormatTransaction(txInfo.tx, txInfo.receipt, contractCode) + transaction, rosettaError = FormatTransaction(txInfo.tx, txInfo.receipt, contractInfo) if rosettaError != nil { return nil, rosettaError } @@ -223,6 +253,58 @@ func (s *BlockAPI) getTransactionInfo( }, nil } +var ( + // defaultTraceReExec is the number of blocks the tracer can go back and re-execute to produce historical state. + // Only 1 block is needed to check for internal transactions + defaultTraceReExec = uint64(1) + // defaultTraceTimeout is the amount of time a transaction can execute + defaultTraceTimeout = (10 * time.Second).String() + // defaultTraceLogConfig is the log config of all traces + defaultTraceLogConfig = vm.LogConfig{ + DisableMemory: false, + DisableStack: false, + DisableStorage: false, + Debug: false, + Limit: 0, + } +) + +// getTransactionTrace for the given txInfo. +func (s *BlockAPI) getTransactionTrace( + ctx context.Context, blk *hmytypes.Block, txInfo *transactionInfo, +) (*hmy.ExecutionResult, *types.Error) { + cacheKey := types.Hash(blk) + types.Hash(txInfo) + if value, ok := s.txTraceCache.Get(cacheKey); ok { + return value.(*hmy.ExecutionResult), nil + } + + msg, vmctx, statedb, err := s.hmy.ComputeTxEnv(blk, int(txInfo.txIndex), defaultTraceReExec) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + execResultInterface, err := s.hmy.TraceTx(ctx, msg, vmctx, statedb, &hmy.TraceConfig{ + LogConfig: &defaultTraceLogConfig, + Timeout: &defaultTraceTimeout, + Reexec: &defaultTraceReExec, + }) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + execResult, ok := execResultInterface.(*hmy.ExecutionResult) + if !ok { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "unknown tracer exec result type", + }) + } + s.txTraceCache.Add(cacheKey, execResult) + + return execResult, nil +} + // getBlock .. func getBlock( ctx context.Context, hmy *hmy.Harmony, blockID *types.PartialBlockIdentifier, diff --git a/rosetta/services/construction_parse.go b/rosetta/services/construction_parse.go index 69212530e..c826a75db 100644 --- a/rosetta/services/construction_parse.go +++ b/rosetta/services/construction_parse.go @@ -53,7 +53,9 @@ func parseUnsignedTransaction( intendedReceipt := &hmyTypes.Receipt{ GasUsed: tx.Gas(), } - formattedTx, rosettaError := FormatTransaction(tx, intendedReceipt, wrappedTransaction.ContractCode) + formattedTx, rosettaError := FormatTransaction( + tx, intendedReceipt, &ContractInfo{ContractCode: wrappedTransaction.ContractCode}, + ) if rosettaError != nil { return nil, rosettaError } @@ -94,7 +96,9 @@ func parseSignedTransaction( intendedReceipt := &hmyTypes.Receipt{ GasUsed: tx.Gas(), } - formattedTx, rosettaError := FormatTransaction(tx, intendedReceipt, wrappedTransaction.ContractCode) + formattedTx, rosettaError := FormatTransaction( + tx, intendedReceipt, &ContractInfo{ContractCode: wrappedTransaction.ContractCode}, + ) if rosettaError != nil { return nil, rosettaError } diff --git a/rosetta/services/construction_parse_test.go b/rosetta/services/construction_parse_test.go index 7b4714117..06d774255 100644 --- a/rosetta/services/construction_parse_test.go +++ b/rosetta/services/construction_parse_test.go @@ -38,7 +38,7 @@ func TestParseUnsignedTransaction(t *testing.T) { refTestReceipt := &hmytypes.Receipt{ GasUsed: testTx.Gas(), } - refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, []byte{}) + refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, &ContractInfo{}) if rosettaError != nil { t.Fatal(rosettaError) } @@ -101,7 +101,7 @@ func TestParseSignedTransaction(t *testing.T) { refTestReceipt := &hmytypes.Receipt{ GasUsed: testTx.Gas(), } - refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, []byte{}) + refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, &ContractInfo{}) if rosettaError != nil { t.Fatal(rosettaError) } diff --git a/rosetta/services/mempool.go b/rosetta/services/mempool.go index c46895e3f..6b368f65a 100644 --- a/rosetta/services/mempool.go +++ b/rosetta/services/mempool.go @@ -84,7 +84,7 @@ func (s *MempoolAPI) MempoolTransaction( GasUsed: poolTx.Gas(), } - respTx, err := FormatTransaction(poolTx, estReceipt, []byte{}) + respTx, err := FormatTransaction(poolTx, estReceipt, &ContractInfo{}) if err != nil { return nil, err } diff --git a/rosetta/services/tx_format.go b/rosetta/services/tx_format.go index 9f32228aa..da68d60cd 100644 --- a/rosetta/services/tx_format.go +++ b/rosetta/services/tx_format.go @@ -9,6 +9,7 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/hmy" "github.com/harmony-one/harmony/rosetta/common" stakingTypes "github.com/harmony-one/harmony/staking/types" ) @@ -18,9 +19,18 @@ var ( FormatDefaultSenderAddress = ethcommon.HexToAddress("0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE") ) +// ContractInfo contains all relevant data for formatting/inspecting transactions involving contracts +type ContractInfo struct { + // ContractAddress is the address of the primary (or first) contract related to the tx. + ContractAddress *ethcommon.Address `json:"contract_hex_address"` + // ContractCode is the code of the primary (or first) contract related to the tx. + ContractCode []byte `json:"contract_code"` + ExecutionResult *hmy.ExecutionResult `json:"execution_result"` +} + // FormatTransaction for staking, cross-shard sender, and plain transactions func FormatTransaction( - tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt, contractCode []byte, + tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt, contractInfo *ContractInfo, ) (fmtTx *types.Transaction, rosettaError *types.Error) { var operations []*types.Operation var isCrossShard, isStaking, isContractCreation bool @@ -39,7 +49,7 @@ func FormatTransaction( case *hmytypes.Transaction: isStaking = false plainTx := tx.(*hmytypes.Transaction) - operations, rosettaError = GetNativeOperationsFromTransaction(plainTx, receipt) + operations, rosettaError = GetNativeOperationsFromTransaction(plainTx, receipt, contractInfo) if rosettaError != nil { return nil, rosettaError } @@ -62,7 +72,7 @@ func FormatTransaction( return nil, rosettaError } txMetadata.ContractAccountIdentifier = contractID - } else if len(contractCode) > 0 && tx.To() != nil { + } else if contractInfo.ContractAddress != nil && len(contractInfo.ContractCode) > 0 { // Contract code was found, so receiving account must be the contract address contractID, rosettaError := newAccountIdentifier(*tx.To()) if rosettaError != nil { diff --git a/rosetta/services/tx_format_test.go b/rosetta/services/tx_format_test.go index 1ef74468f..c6e7edb5a 100644 --- a/rosetta/services/tx_format_test.go +++ b/rosetta/services/tx_format_test.go @@ -17,11 +17,10 @@ import ( "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 { + if _, ok := common.MutuallyExclusiveOperations[op.Type]; !ok { continue } if foundType == "" { @@ -79,7 +78,7 @@ func testFormatStakingTransaction( Status: hmytypes.ReceiptStatusSuccessful, GasUsed: gasUsed, } - rosettaTx, rosettaError := FormatTransaction(tx, receipt, []byte{}) + rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}) if rosettaError != nil { t.Fatal(rosettaError) } @@ -134,7 +133,7 @@ func testFormatPlainTransaction( Status: hmytypes.ReceiptStatusSuccessful, GasUsed: gasUsed, } - rosettaTx, rosettaError := FormatTransaction(tx, receipt, []byte{}) + rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}) if rosettaError != nil { t.Fatal(rosettaError) } @@ -191,7 +190,7 @@ func testFormatCrossShardSenderTransaction( Status: hmytypes.ReceiptStatusSuccessful, GasUsed: gasUsed, } - rosettaTx, rosettaError := FormatTransaction(tx, receipt, []byte{}) + rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}) if rosettaError != nil { t.Fatal(rosettaError) } diff --git a/rosetta/services/tx_operation.go b/rosetta/services/tx_operation.go index a8cdf430f..2224721c0 100644 --- a/rosetta/services/tx_operation.go +++ b/rosetta/services/tx_operation.go @@ -9,8 +9,9 @@ import ( "github.com/harmony-one/harmony/core" hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/hmy" - "github.com/harmony-one/harmony/internal/utils" + "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" @@ -18,10 +19,10 @@ import ( ) // GetNativeOperationsFromTransaction for one of the following transactions: -// contract creation, cross-shard sender, same-shard transfer. +// contract creation, cross-shard sender, same-shard transfer with and without code execution. // Native operations only include operations that affect the native currency balance of an account. func GetNativeOperationsFromTransaction( - tx *hmytypes.Transaction, receipt *hmytypes.Receipt, + tx *hmytypes.Transaction, receipt *hmytypes.Receipt, contractInfo *ContractInfo, ) ([]*types.Operation, *types.Error) { senderAddress, err := tx.SenderAddress() if err != nil { @@ -35,20 +36,25 @@ func GetNativeOperationsFromTransaction( // All operations excepts for cross-shard tx payout expend gas gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice()) gasOperations := newNativeOperationsWithGas(gasExpended, accountID) + startingOpIndex := gasOperations[0].OperationIdentifier.Index + 1 - // Handle different cases of plain transactions + // Handle based on tx type & available data. var txOperations []*types.Operation if tx.To() == nil { - txOperations, rosettaError = newContractCreationNativeOperations( - gasOperations[0].OperationIdentifier, tx, receipt, senderAddress, + txOperations, rosettaError = getContractCreationNativeOperations( + tx, receipt, senderAddress, contractInfo, &startingOpIndex, ) } else if tx.ShardID() != tx.ToShardID() { - txOperations, rosettaError = newCrossShardSenderTransferNativeOperations( - gasOperations[0].OperationIdentifier, tx, senderAddress, + txOperations, rosettaError = getCrossShardSenderTransferNativeOperations( + tx, senderAddress, &startingOpIndex, + ) + } else if contractInfo != nil && contractInfo.ExecutionResult != nil { + txOperations, rosettaError = getContractTransferNativeOperations( + tx, receipt, senderAddress, tx.To(), contractInfo, &startingOpIndex, ) } else { - txOperations, rosettaError = newTransferNativeOperations( - gasOperations[0].OperationIdentifier, tx, receipt, senderAddress, + txOperations, rosettaError = getBasicTransferNativeOperations( + tx, receipt, senderAddress, tx.To(), &startingOpIndex, ) } if rosettaError != nil { @@ -59,7 +65,7 @@ func GetNativeOperationsFromTransaction( } // GetNativeOperationsFromStakingTransaction for all staking directives -// Note that only native operations can come from staking transactions. +// Note that only native token operations can come from staking transactions. func GetNativeOperationsFromStakingTransaction( tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt, ) ([]*types.Operation, *types.Error) { @@ -116,11 +122,8 @@ func GetNativeOperationsFromStakingTransaction( OperationIdentifier: &types.OperationIdentifier{ Index: gasOperations[0].OperationIdentifier.Index + 1, }, - RelatedOperations: []*types.OperationIdentifier{ - gasOperations[0].OperationIdentifier, - }, Type: tx.StakingType().String(), - Status: common.SuccessOperationStatus.Status, + Status: GetTransactionStatus(tx, receipt), Account: accountID, Amount: amount, Metadata: metadata, @@ -161,7 +164,207 @@ func GetSideEffectOperationsFromGenesisSpec( ) } -// getSideEffectOperationsFromValueMap is a helper for side effect operation construction from a value map. +// GetTransactionStatus for any valid harmony transaction given its receipt. +func GetTransactionStatus(tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt) string { + if _, ok := tx.(*hmytypes.Transaction); ok { + status := common.SuccessOperationStatus.Status + if receipt.Status == hmytypes.ReceiptStatusFailed { + if len(tx.Data()) == 0 && receipt.CumulativeGasUsed <= params.TxGas { + status = common.FailureOperationStatus.Status + } else { + status = common.ContractFailureOperationStatus.Status + } + } + return status + } else if _, ok := tx.(*stakingTypes.StakingTransaction); ok { + return common.SuccessOperationStatus.Status + } + // Type of tx unknown, so default to failure + return common.FailureOperationStatus.Status +} + +// getBasicTransferNativeOperations extracts & formats the basic native operations for non-staking transaction. +// Note that this does NOT include any contract related transfers (i.e: internal transactions). +func getBasicTransferNativeOperations( + tx *hmytypes.Transaction, receipt *hmytypes.Receipt, senderAddress ethcommon.Address, toAddress *ethcommon.Address, + startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + if toAddress == nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "tx receiver not found", + }) + } + + // Common operation elements + status := GetTransactionStatus(tx, receipt) + from, rosettaError := newAccountIdentifier(senderAddress) + if rosettaError != nil { + return nil, rosettaError + } + to, rosettaError := newAccountIdentifier(*toAddress) + if rosettaError != nil { + return nil, rosettaError + } + + return newSameShardTransferNativeOperations(from, to, tx.Value(), status, startingOperationIndex), nil +} + +// getContractTransferNativeOperations extracts & formats the native operations for any +// transaction involving a contract. +// Note that this will include any native tokens that were transferred from the contract (i.e: internal transactions). +func getContractTransferNativeOperations( + tx *hmytypes.Transaction, receipt *hmytypes.Receipt, senderAddress ethcommon.Address, toAddress *ethcommon.Address, + contractInfo *ContractInfo, startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + basicOps, rosettaError := getBasicTransferNativeOperations( + tx, receipt, senderAddress, toAddress, startingOperationIndex, + ) + if rosettaError != nil { + return nil, rosettaError + } + + status := GetTransactionStatus(tx, receipt) + startingIndex := basicOps[len(basicOps)-1].OperationIdentifier.Index + 1 + internalTxOps, rosettaError := getContractInternalTransferNativeOperations( + contractInfo.ExecutionResult, status, &startingIndex, + ) + if rosettaError != nil { + return nil, rosettaError + } + + return append(basicOps, internalTxOps...), nil +} + +// getContractCreationNativeOperations extracts & formats the native operations for a contract creation tx +func getContractCreationNativeOperations( + tx *hmytypes.Transaction, receipt *hmytypes.Receipt, senderAddress ethcommon.Address, contractInfo *ContractInfo, + startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + basicOps, rosettaError := getBasicTransferNativeOperations( + tx, receipt, senderAddress, &receipt.ContractAddress, startingOperationIndex, + ) + if rosettaError != nil { + return nil, rosettaError + } + for _, op := range basicOps { + op.Type = common.ContractCreationOperation + } + + status := GetTransactionStatus(tx, receipt) + startingIndex := basicOps[len(basicOps)-1].OperationIdentifier.Index + 1 + internalTxOps, rosettaError := getContractInternalTransferNativeOperations( + contractInfo.ExecutionResult, status, &startingIndex, + ) + if rosettaError != nil { + return nil, rosettaError + } + + return append(basicOps, internalTxOps...), nil +} + +var ( + // internalNativeTransferEvmOps are the EVM operations that can execute a native transfer + // where the sender is a contract address. This is also known as ops for an 'internal' transaction. + // All operations have at least 7 elements on the stack when executed. + internalNativeTransferEvmOps = map[string]interface{}{ + vm.CALL.String(): struct{}{}, + vm.CALLCODE.String(): struct{}{}, + } +) + +// getContractInternalTransferNativeOperations extracts & formats the native operations for a contract's internal +// native token transfers (i.e: the sender of a transaction is the contract). +func getContractInternalTransferNativeOperations( + executionResult *hmy.ExecutionResult, status string, + startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + ops := []*types.Operation{} + if executionResult == nil { + // No error since nil execution result implies empty StructLogs, which is not an error. + return ops, nil + } + + for _, log := range executionResult.StructLogs { + if _, ok := internalNativeTransferEvmOps[log.Op]; ok { + fromAccID, rosettaError := newAccountIdentifier(log.ContractAddress) + if rosettaError != nil { + return nil, rosettaError + } + topIndex := len(log.Stack) - 1 + toAccID, rosettaError := newAccountIdentifier(ethcommon.HexToAddress(log.Stack[topIndex-1])) + if rosettaError != nil { + return nil, rosettaError + } + value, ok := new(big.Int).SetString(log.Stack[topIndex-2], 16) + if !ok { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": fmt.Sprintf("unable to set value amount, raw: %v", log.Stack[topIndex-2]), + }) + } + + ops = append( + ops, newSameShardTransferNativeOperations(fromAccID, toAccID, value, status, startingOperationIndex)..., + ) + nextOpIndex := ops[len(ops)-1].OperationIdentifier.Index + 1 + startingOperationIndex = &nextOpIndex + } + } + + return ops, nil +} + +// getCrossShardSenderTransferNativeOperations extracts & formats the native operation(s) +// for cross-shard-tx on the sender's shard. +func getCrossShardSenderTransferNativeOperations( + tx *hmytypes.Transaction, senderAddress ethcommon.Address, + startingOperationIndex *int64, +) ([]*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(), + }) + } + + var opIndex int64 + if startingOperationIndex != nil { + opIndex = *startingOperationIndex + } else { + opIndex = 0 + } + + return []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: opIndex, + }, + Type: common.NativeCrossShardTransferOperation, + Status: common.SuccessOperationStatus.Status, + Account: senderAccountID, + Amount: &types.Amount{ + Value: negativeBigValue(tx.Value()), + Currency: &common.NativeCurrency, + }, + Metadata: metadata, + }, + }, nil +} + +// getSideEffectOperationsFromValueMap is a helper for side effect operation construction from a address to value map. func getSideEffectOperationsFromValueMap( valueMap map[ethcommon.Address]*big.Int, opType string, startingOperationIndex *int64, ) ([]*types.Operation, *types.Error) { @@ -263,159 +466,23 @@ func getAmountFromCollectRewards( 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.NativeTransferOperation - 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()) - } - } - +// newSameShardTransferNativeOperations creates a new slice of operations for a native transfer on the same shard. +func newSameShardTransferNativeOperations( + from, to *types.AccountIdentifier, amount *big.Int, status string, + startingOperationIndex *int64, +) []*types.Operation { // 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.NativeCrossShardTransferOperation, - 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) { - // TODO: correct the contract creation transaction... - - // 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 + var opIndex int64 + if startingOperationIndex != nil { + opIndex = *startingOperationIndex + } else { + opIndex = 0 } - - // Subtraction operation elements subOperationID := &types.OperationIdentifier{ - Index: startingOperationID.Index + 1, - } - subRelatedID := []*types.OperationIdentifier{ - startingOperationID, - } - subAccountID, rosettaError := newAccountIdentifier(senderAddress) - if rosettaError != nil { - return nil, rosettaError + Index: opIndex, } subAmount := &types.Amount{ - Value: negativeBigValue(tx.Value()), + Value: negativeBigValue(amount), Currency: &common.NativeCurrency, } @@ -427,28 +494,27 @@ func newContractCreationNativeOperations( subOperationID, } addAmount := &types.Amount{ - Value: tx.Value().String(), + Value: amount.String(), Currency: &common.NativeCurrency, } return []*types.Operation{ { OperationIdentifier: subOperationID, - RelatedOperations: subRelatedID, - Type: common.ContractCreationOperation, + Type: common.NativeTransferOperation, Status: status, - Account: subAccountID, + Account: from, Amount: subAmount, }, { OperationIdentifier: addOperationID, RelatedOperations: addRelatedID, - Type: common.ContractCreationOperation, + Type: common.NativeTransferOperation, Status: status, - Account: contractAddressID, + Account: to, Amount: addAmount, }, - }, nil + } } // newNativeOperationsWithGas creates a new operation with the gas fee as the first operation. diff --git a/rosetta/services/tx_operation_test.go b/rosetta/services/tx_operation_test.go index 75351f391..3ba7d55ae 100644 --- a/rosetta/services/tx_operation_test.go +++ b/rosetta/services/tx_operation_test.go @@ -11,6 +11,8 @@ import ( "github.com/ethereum/go-ethereum/crypto" hmytypes "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/core/vm" + "github.com/harmony-one/harmony/hmy" internalCommon "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/rosetta/common" @@ -63,12 +65,9 @@ func TestGetStakingOperationsFromCreateValidator(t *testing.T) { refOperations := newNativeOperationsWithGas(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, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, Amount: &types.Amount{ Value: negativeBigValue(tenOnes), Currency: &common.NativeCurrency, @@ -124,6 +123,9 @@ func TestGetSideEffectOperationsFromValueMap(t *testing.T) { t.Errorf("operation %v has wrong currency", i) } } + if err := assertNativeOperationTypeUniquenessInvariant(ops); err != nil { + t.Error(err) + } testStartingIndex := int64(12) ops, rosettaError = getSideEffectOperationsFromValueMap(testPayouts, testType, &testStartingIndex) @@ -153,6 +155,9 @@ func TestGetSideEffectOperationsFromValueMap(t *testing.T) { t.Errorf("operation %v has wrong currency", i) } } + if err := assertNativeOperationTypeUniquenessInvariant(ops); err != nil { + t.Error(err) + } } func TestGetStakingOperationsFromDelegate(t *testing.T) { @@ -195,12 +200,9 @@ func TestGetStakingOperationsFromDelegate(t *testing.T) { refOperations := newNativeOperationsWithGas(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, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, Amount: &types.Amount{ Value: negativeBigValue(tenOnes), Currency: &common.NativeCurrency, @@ -259,12 +261,9 @@ func TestGetStakingOperationsFromUndelegate(t *testing.T) { refOperations := newNativeOperationsWithGas(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, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, Amount: &types.Amount{ Value: fmt.Sprintf("0"), Currency: &common.NativeCurrency, @@ -323,12 +322,9 @@ func TestGetStakingOperationsFromCollectRewards(t *testing.T) { refOperations := newNativeOperationsWithGas(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, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, Amount: &types.Amount{ Value: fmt.Sprintf("%v", tenOnes.Uint64()), Currency: &common.NativeCurrency, @@ -380,12 +376,9 @@ func TestGetStakingOperationsFromEditValidator(t *testing.T) { refOperations := newNativeOperationsWithGas(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, + Type: tx.StakingType().String(), + Status: common.SuccessOperationStatus.Status, + Account: senderAccID, Amount: &types.Amount{ Value: fmt.Sprintf("0"), Currency: &common.NativeCurrency, @@ -404,7 +397,7 @@ func TestGetStakingOperationsFromEditValidator(t *testing.T) { } } -func TestNewTransferNativeOperations(t *testing.T) { +func TestGetBasicTransferOperations(t *testing.T) { signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) tx, err := helpers.CreateTestTransaction( signer, 0, 0, 0, 1e18, gasPrice, big.NewInt(1), []byte("test"), @@ -432,11 +425,6 @@ func TestNewTransferNativeOperations(t *testing.T) { OperationIdentifier: &types.OperationIdentifier{ Index: startingOpID.Index + 1, }, - RelatedOperations: []*types.OperationIdentifier{ - { - Index: startingOpID.Index, - }, - }, Type: common.NativeTransferOperation, Status: common.ContractFailureOperationStatus.Status, Account: senderAccID, @@ -466,7 +454,8 @@ func TestNewTransferNativeOperations(t *testing.T) { receipt := &hmytypes.Receipt{ Status: hmytypes.ReceiptStatusFailed, } - operations, rosettaError := newTransferNativeOperations(startingOpID, tx, receipt, senderAddr) + opIndex := startingOpID.Index + 1 + operations, rosettaError := getBasicTransferNativeOperations(tx, receipt, senderAddr, tx.To(), &opIndex) if rosettaError != nil { t.Fatal(rosettaError) } @@ -481,7 +470,7 @@ func TestNewTransferNativeOperations(t *testing.T) { refOperations[0].Status = common.SuccessOperationStatus.Status refOperations[1].Status = common.SuccessOperationStatus.Status receipt.Status = hmytypes.ReceiptStatusSuccessful - operations, rosettaError = newTransferNativeOperations(startingOpID, tx, receipt, senderAddr) + operations, rosettaError = getBasicTransferNativeOperations(tx, receipt, senderAddr, tx.To(), &opIndex) if rosettaError != nil { t.Fatal(rosettaError) } @@ -493,7 +482,7 @@ func TestNewTransferNativeOperations(t *testing.T) { } } -func TestNewCrossShardSenderTransferNativeOperations(t *testing.T) { +func TestGetCrossShardSenderTransferNativeOperations(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"), @@ -527,9 +516,6 @@ func TestNewCrossShardSenderTransferNativeOperations(t *testing.T) { OperationIdentifier: &types.OperationIdentifier{ Index: startingOpID.Index + 1, }, - RelatedOperations: []*types.OperationIdentifier{ - startingOpID, - }, Type: common.NativeCrossShardTransferOperation, Status: common.SuccessOperationStatus.Status, Account: senderAccID, @@ -540,7 +526,8 @@ func TestNewCrossShardSenderTransferNativeOperations(t *testing.T) { Metadata: metadata, }, } - operations, rosettaError := newCrossShardSenderTransferNativeOperations(startingOpID, tx, senderAddr) + opIndex := startingOpID.Index + 1 + operations, rosettaError := getCrossShardSenderTransferNativeOperations(tx, senderAddr, &opIndex) if rosettaError != nil { t.Fatal(rosettaError) } @@ -552,7 +539,365 @@ func TestNewCrossShardSenderTransferNativeOperations(t *testing.T) { } } -func TestNewContractCreationNativeOperations(t *testing.T) { +var ( + testExecResultForInternalTx = &hmy.ExecutionResult{ + StructLogs: []hmy.StructLogRes{ + { + Pc: 1316, + Op: "DUP9", + CallerAddress: ethcommon.HexToAddress("0x2a44f609f860d4ff8835f9ec1d9b1acdae1fd9cb"), + ContractAddress: ethcommon.HexToAddress("0x4c4fde977fbbe722cddf5719d7edd488510be16a"), + Gas: 6677398, + GasCost: 3, + Depth: 1, + Stack: []string{ + "0000000000000000000000000000000000000000000000000000000023024408", + "000000000000000000000000000000000000000000000000000000000000019a", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000050", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000021e19e0c9bab2400000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000080", + "00000000000000000000000000000000000000000000021e19e0c9bab2400000", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + }, + Memory: []string{ + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000003", + "0000000000000000000000000000000000000000000000000000000000000080", + }, + Storage: map[string]string{ + "43a43725d4b041c11b63ea10be0771546465a0c0654fd13c2712f2a8ce3f8b85": "0000000000000000000000000000000000000000000000000000000000000050", + }, + }, + { + Pc: 1317, + Op: vm.CALL.String(), + CallerAddress: ethcommon.HexToAddress("0x2a44f609f860d4ff8835f9ec1d9b1acdae1fd9cb"), + ContractAddress: ethcommon.HexToAddress("0x4c4fde977fbbe722cddf5719d7edd488510be16a"), + Gas: 6677395, + GasCost: 9700, + Depth: 1, + Stack: []string{ + "0000000000000000000000000000000000000000000000000000000023024408", + "000000000000000000000000000000000000000000000000000000000000019a", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000050", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000021e19e0c9bab2400000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000002710", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000000", + }, + Memory: []string{ + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000003", + "0000000000000000000000000000000000000000000000000000000000000080", + }, + Storage: map[string]string{ + "43a43725d4b041c11b63ea10be0771546465a0c0654fd13c2712f2a8ce3f8b85": "0000000000000000000000000000000000000000000000000000000000000050", + }, + }, + { + Pc: 1318, + Op: "SWAP4", + CallerAddress: ethcommon.HexToAddress("0x2a44f609f860d4ff8835f9ec1d9b1acdae1fd9cb"), + ContractAddress: ethcommon.HexToAddress("0x4c4fde977fbbe722cddf5719d7edd488510be16a"), + Gas: 6669995, + GasCost: 3, + Depth: 1, + Stack: []string{ + "0000000000000000000000000000000000000000000000000000000023024408", + "000000000000000000000000000000000000000000000000000000000000019a", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000050", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000021e19e0c9bab2400000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000001", + }, + Memory: []string{ + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000003", + "0000000000000000000000000000000000000000000000000000000000000080", + }, + Storage: map[string]string{ + "43a43725d4b041c11b63ea10be0771546465a0c0654fd13c2712f2a8ce3f8b85": "0000000000000000000000000000000000000000000000000000000000000050", + }, + }, + { + Pc: 1319, + Op: vm.CALLCODE.String(), + CallerAddress: ethcommon.HexToAddress("0x2a44f609f860d4ff8835f9ec1d9b1acdae1fd9cb"), + ContractAddress: ethcommon.HexToAddress("0x4c4fde977fbbe722cddf5719d7edd488510be16a"), + Gas: 6677395, + GasCost: 9700, + Depth: 1, + Stack: []string{ + "0000000000000000000000000000000000000000000000000000000023024408", + "000000000000000000000000000000000000000000000000000000000000019a", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000050", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000021e19e0c9bab2400000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000002710", + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000000", + }, + Memory: []string{ + "0000000000000000000000007c41e0668b551f4f902cfaec05b5bdca68b124ce", + "0000000000000000000000000000000000000000000000000000000000000003", + "0000000000000000000000000000000000000000000000000000000000000080", + }, + Storage: map[string]string{ + "43a43725d4b041c11b63ea10be0771546465a0c0654fd13c2712f2a8ce3f8b85": "0000000000000000000000000000000000000000000000000000000000000050", + }, + }, + }, + } + testExecResultForInternalTxValueSum = uint64(20000) +) + +func TestGetContractInternalTransferNativeOperations(t *testing.T) { + refStatus := common.SuccessOperationStatus.Status + refStartingIndex := int64(23) + baseValidation := func(ops []*types.Operation, expectedValueSum uint64) { + prevIndex := int64(-1) + valueSum := int64(0) + absValueSum := uint64(0) + for i, op := range ops { + if op.OperationIdentifier.Index <= prevIndex { + t.Errorf("expect prev index (%v) < curr index (%v) for op %v", + prevIndex, op.OperationIdentifier.Index, i, + ) + } + prevIndex = op.OperationIdentifier.Index + if op.Status != refStatus { + t.Errorf("wrong status for op %v", i) + } + if op.Type != common.NativeTransferOperation { + t.Errorf("wrong operation type for op %v", i) + } + if types.Hash(op.Amount.Currency) != common.NativeCurrencyHash { + t.Errorf("wrong currency for op %v", i) + } + val, err := types.AmountValue(op.Amount) + if err != nil { + t.Error(err) + } + valueSum += val.Int64() + absValueSum += val.Abs(val).Uint64() + } + + if valueSum != 0 { + t.Errorf("expected sum of all non-gas values to be 0") + } + if expectedValueSum*2 != absValueSum { + t.Errorf("sum of all positive values of operations do not match execpted sum of values") + } + } + + testOps, rosettaError := getContractInternalTransferNativeOperations( + testExecResultForInternalTx, refStatus, &refStartingIndex, + ) + if rosettaError != nil { + t.Error(rosettaError) + } + baseValidation(testOps, testExecResultForInternalTxValueSum) + if len(testOps) == 0 { + t.Errorf("expect atleast 1 operation") + } + if testOps[0].OperationIdentifier.Index != refStartingIndex { + t.Errorf("expected starting index to be %v", refStartingIndex) + } + if err := assertNativeOperationTypeUniquenessInvariant(testOps); err != nil { + t.Error(err) + } + + testOps, rosettaError = getContractInternalTransferNativeOperations( + testExecResultForInternalTx, refStatus, nil, + ) + if rosettaError != nil { + t.Error(rosettaError) + } + baseValidation(testOps, testExecResultForInternalTxValueSum) + if len(testOps) == 0 { + t.Errorf("expect atleast 1 operation") + } + if testOps[0].OperationIdentifier.Index != 0 { + t.Errorf("expected starting index to be 0") + } + if err := assertNativeOperationTypeUniquenessInvariant(testOps); err != nil { + t.Error(err) + } + + testOps, rosettaError = getContractInternalTransferNativeOperations( + nil, refStatus, nil, + ) + if rosettaError != nil { + t.Error(rosettaError) + } + if len(testOps) != 0 { + t.Errorf("expected len 0 test operations for nil execution result") + } + if err := assertNativeOperationTypeUniquenessInvariant(testOps); err != nil { + t.Error(err) + } + + testOps, rosettaError = getContractInternalTransferNativeOperations( + &hmy.ExecutionResult{}, refStatus, nil, + ) + if rosettaError != nil { + t.Error(rosettaError) + } + if len(testOps) != 0 { + t.Errorf("expected len 0 test operations for nil struct logs") + } + if err := assertNativeOperationTypeUniquenessInvariant(testOps); err != nil { + t.Error(err) + } +} + +func TestGetContractTransferNativeOperations(t *testing.T) { + signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID) + refTxValue := big.NewInt(1) + refTx, err := helpers.CreateTestTransaction( + signer, 0, 0, 0, 1e18, gasPrice, refTxValue, []byte("blah-blah-blah"), + ) + if err != nil { + t.Fatal(err.Error()) + } + refSenderAddr, err := refTx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + refStatus := common.SuccessOperationStatus.Status + refStartingIndex := int64(23) + refReceipt := &hmytypes.Receipt{ + PostState: nil, + Status: 1, + GasUsed: params.TxGas * 3, // somme arb number > TxGas + } + baseValidation := func(ops []*types.Operation, expectedValueSum uint64) { + prevIndex := int64(-1) + valueSum := int64(0) + absValueSum := uint64(0) + for i, op := range ops { + if op.OperationIdentifier.Index <= prevIndex { + t.Errorf("expect prev index (%v) < curr index (%v) for op %v", + prevIndex, op.OperationIdentifier.Index, i, + ) + } + prevIndex = op.OperationIdentifier.Index + if op.Status != refStatus { + t.Errorf("wrong status for op %v", i) + } + if types.Hash(op.Amount.Currency) != common.NativeCurrencyHash { + t.Errorf("wrong currency for op %v", i) + } + if op.Type == common.ExpendGasOperation { + continue + } + if op.Type != common.NativeTransferOperation { + t.Errorf("wrong operation type for op %v", i) + } + val, err := types.AmountValue(op.Amount) + if err != nil { + t.Error(err) + } + valueSum += val.Int64() + absValueSum += val.Abs(val).Uint64() + } + + if valueSum != 0 { + t.Errorf("expected sum of all non-gas values to be 0") + } + if expectedValueSum*2 != absValueSum { + t.Errorf("sum of all positive values of operations do not match execpted sum of values") + } + } + + testOps, rosettaError := getContractTransferNativeOperations( + refTx, refReceipt, refSenderAddr, refTx.To(), + &ContractInfo{ExecutionResult: testExecResultForInternalTx}, &refStartingIndex, + ) + if rosettaError != nil { + t.Error(rosettaError) + } + baseValidation(testOps, testExecResultForInternalTxValueSum+refTxValue.Uint64()) + if len(testOps) == 0 { + t.Errorf("expect atleast 1 operation") + } + if testOps[0].OperationIdentifier.Index != refStartingIndex { + t.Errorf("expected starting index to be %v", refStartingIndex) + } + if err := assertNativeOperationTypeUniquenessInvariant(testOps); err != nil { + t.Error(err) + } + + testOps, rosettaError = getContractTransferNativeOperations( + refTx, refReceipt, refSenderAddr, refTx.To(), + &ContractInfo{ExecutionResult: testExecResultForInternalTx}, nil, + ) + if rosettaError != nil { + t.Error(rosettaError) + } + baseValidation(testOps, testExecResultForInternalTxValueSum+refTxValue.Uint64()) + if len(testOps) == 0 { + t.Errorf("expect atleast 1 operation") + } + if testOps[0].OperationIdentifier.Index != 0 { + t.Errorf("expected starting index to be 0") + } + if err := assertNativeOperationTypeUniquenessInvariant(testOps); err != nil { + t.Error(err) + } + + testOps, rosettaError = getContractTransferNativeOperations( + refTx, refReceipt, refSenderAddr, refTx.To(), + &ContractInfo{}, nil, + ) + if rosettaError != nil { + t.Error(rosettaError) + } + baseValidation(testOps, refTxValue.Uint64()) + if len(testOps) == 0 { + t.Errorf("expect atleast 1 operation") + } + if testOps[0].OperationIdentifier.Index != 0 { + t.Errorf("expected starting index to be 0") + } + if len(testOps) > 3 { + t.Errorf("expect at most 3 operations for nil ExecutionResult") + } + if err := assertNativeOperationTypeUniquenessInvariant(testOps); err != nil { + t.Error(err) + } +} + +func TestGetContractCreationNativeOperations(t *testing.T) { dummyContractKey, err := crypto.GenerateKey() if err != nil { t.Fatalf(err.Error()) @@ -586,9 +931,6 @@ func TestNewContractCreationNativeOperations(t *testing.T) { OperationIdentifier: &types.OperationIdentifier{ Index: startingOpID.Index + 1, }, - RelatedOperations: []*types.OperationIdentifier{ - startingOpID, - }, Type: common.ContractCreationOperation, Status: common.ContractFailureOperationStatus.Status, Account: senderAccID, @@ -619,7 +961,8 @@ func TestNewContractCreationNativeOperations(t *testing.T) { Status: hmytypes.ReceiptStatusFailed, ContractAddress: contractAddr, } - operations, rosettaError := newContractCreationNativeOperations(startingOpID, tx, receipt, senderAddr) + opIndex := startingOpID.Index + 1 + operations, rosettaError := getContractCreationNativeOperations(tx, receipt, senderAddr, &ContractInfo{}, &opIndex) if rosettaError != nil { t.Fatal(rosettaError) } @@ -634,7 +977,7 @@ func TestNewContractCreationNativeOperations(t *testing.T) { refOperations[0].Status = common.SuccessOperationStatus.Status refOperations[1].Status = common.SuccessOperationStatus.Status receipt.Status = hmytypes.ReceiptStatusSuccessful // Indicate successful tx - operations, rosettaError = newContractCreationNativeOperations(startingOpID, tx, receipt, senderAddr) + operations, rosettaError = getContractCreationNativeOperations(tx, receipt, senderAddr, &ContractInfo{}, &opIndex) if rosettaError != nil { t.Fatal(rosettaError) } @@ -644,6 +987,62 @@ func TestNewContractCreationNativeOperations(t *testing.T) { if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { t.Error(err) } + + traceValidation := func(ops []*types.Operation, expectedValueSum uint64) { + prevIndex := int64(-1) + valueSum := int64(0) + absValueSum := uint64(0) + for i, op := range ops { + if op.OperationIdentifier.Index <= prevIndex { + t.Errorf("expect prev index (%v) < curr index (%v) for op %v", + prevIndex, op.OperationIdentifier.Index, i, + ) + } + prevIndex = op.OperationIdentifier.Index + if op.Status != refOperations[0].Status { + t.Errorf("wrong status for op %v", i) + } + if types.Hash(op.Amount.Currency) != common.NativeCurrencyHash { + t.Errorf("wrong currency for op %v", i) + } + if op.Type == common.ExpendGasOperation || op.Type == common.ContractCreationOperation { + continue + } + if op.Type != common.NativeTransferOperation { + t.Errorf("wrong operation type for op %v", i) + } + val, err := types.AmountValue(op.Amount) + if err != nil { + t.Error(err) + } + valueSum += val.Int64() + absValueSum += val.Abs(val).Uint64() + } + + if valueSum != 0 { + t.Errorf("expected sum of all non-gas values to be 0") + } + if expectedValueSum*2 != absValueSum { + t.Errorf("sum of all positive values of operations do not match execpted sum of values") + } + } + operations, rosettaError = getContractCreationNativeOperations( + tx, receipt, senderAddr, &ContractInfo{ExecutionResult: testExecResultForInternalTx}, &opIndex, + ) + if rosettaError != nil { + t.Fatal(rosettaError) + } + traceValidation(operations, testExecResultForInternalTxValueSum) + if len(operations) == 0 { + t.Errorf("expect atleast 1 operation") + } + if operations[0].OperationIdentifier.Index != opIndex { + t.Errorf("expect first operation to be %v", opIndex) + } + if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { + t.Error(err) + } + } func TestNewNativeOperations(t *testing.T) {