diff --git a/evm-ds/Cargo.lock b/evm-ds/Cargo.lock index 74c18868a6..466df14370 100644 --- a/evm-ds/Cargo.lock +++ b/evm-ds/Cargo.lock @@ -616,7 +616,7 @@ dependencies = [ [[package]] name = "evm" version = "0.37.0" -source = "git+https://github.com/Zilliqa/evm?branch=precompile-access-backend#53485a9e76a3b6dc80d7a3dadf47a9784af8e4b0" +source = "git+https://github.com/Zilliqa/evm?branch=precompile-backend-plus-otter#8014eb873a0b400cd64e15f57616abd2b324b68f" dependencies = [ "auto_impl", "environmental", @@ -636,7 +636,7 @@ dependencies = [ [[package]] name = "evm-core" version = "0.37.0" -source = "git+https://github.com/Zilliqa/evm?branch=precompile-access-backend#53485a9e76a3b6dc80d7a3dadf47a9784af8e4b0" +source = "git+https://github.com/Zilliqa/evm?branch=precompile-backend-plus-otter#8014eb873a0b400cd64e15f57616abd2b324b68f" dependencies = [ "parity-scale-codec", "primitive-types 0.12.1", @@ -688,7 +688,7 @@ dependencies = [ [[package]] name = "evm-gasometer" version = "0.37.0" -source = "git+https://github.com/Zilliqa/evm?branch=precompile-access-backend#53485a9e76a3b6dc80d7a3dadf47a9784af8e4b0" +source = "git+https://github.com/Zilliqa/evm?branch=precompile-backend-plus-otter#8014eb873a0b400cd64e15f57616abd2b324b68f" dependencies = [ "environmental", "evm-core", @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "evm-runtime" version = "0.37.0" -source = "git+https://github.com/Zilliqa/evm?branch=precompile-access-backend#53485a9e76a3b6dc80d7a3dadf47a9784af8e4b0" +source = "git+https://github.com/Zilliqa/evm?branch=precompile-backend-plus-otter#8014eb873a0b400cd64e15f57616abd2b324b68f" dependencies = [ "auto_impl", "environmental", diff --git a/evm-ds/Cargo.toml b/evm-ds/Cargo.toml index 5133dcce4a..0334c93528 100644 --- a/evm-ds/Cargo.toml +++ b/evm-ds/Cargo.toml @@ -13,7 +13,7 @@ bytes = "1.1.0" clap = { version = "3.1.6", features = ["derive"] } log4rs = { version = "1.1.1", features = ["all_components", "gzip"] } ethereum = "0.12.0" -evm = { git = "https://github.com/Zilliqa/evm", branch = "precompile-access-backend", features = ["tracing"] } +evm = { git = "https://github.com/Zilliqa/evm", branch = "precompile-backend-plus-otter", features = ["tracing"] } ethabi = "18.0.0" serde = "1.0.152" serde_yaml = "0.8.25" diff --git a/evm-ds/src/cps_executor.rs b/evm-ds/src/cps_executor.rs index ee788bafd1..5bf057fe44 100644 --- a/evm-ds/src/cps_executor.rs +++ b/evm-ds/src/cps_executor.rs @@ -345,6 +345,14 @@ impl<'a> Handler for CpsExecutor<'a> { } } + fn get_create_address( + &mut self, + scheme: CreateScheme, + ) -> H160 { + self.stack_executor.create_address(scheme) + } + + /// Invoke a call operation. fn call( &mut self, diff --git a/evm-ds/src/evm_server_run.rs b/evm-ds/src/evm_server_run.rs index dc910e6ea7..44857aa24d 100644 --- a/evm-ds/src/evm_server_run.rs +++ b/evm-ds/src/evm_server_run.rs @@ -5,10 +5,7 @@ use std::sync::{Arc, Mutex}; use crate::CallContext; use evm::executor::stack::MemoryStackSubstate; -use evm::{ - backend::Apply, - executor::stack::{MemoryStackState, StackSubstateMetadata}, -}; +use evm::{backend::Apply, executor::stack::{MemoryStackState, StackSubstateMetadata}}; use evm::{Machine, Runtime}; use log::{debug, error, info}; @@ -50,9 +47,9 @@ pub async fn run_evm_impl( // panic. (Using the parent runtime and dropping on stack unwind will mess up the parent runtime). tokio::task::spawn_blocking(move || { debug!( - "Running EVM: origin: {:?} address: {:?} gas: {:?} value: {:?} extras: {:?}, estimate: {:?}, cps: {:?}, tx_trace: {:?}", + "Running EVM: origin: {:?} address: {:?} gas: {:?} value: {:?} extras: {:?}, estimate: {:?} is_continuation: {:?}, cps: {:?}, \ntx_trace: {:?}, \ndata: {:02X?}, \ncode: {:02X?}", backend.origin, address, gas_limit, apparent_value, - backend.extras, estimate, enable_cps, tx_trace); + backend.extras, estimate, node_continuation.is_none(), enable_cps, tx_trace, data, code); let code = Rc::new(code); let data = Rc::new(data); // TODO: handle call_l64_after_gas problem: https://zilliqa-jira.atlassian.net/browse/ZIL-5012 @@ -157,7 +154,9 @@ pub async fn run_evm_impl( listener.finished_call(); match exit_reason { - evm::ExitReason::Succeed(_) => {} + evm::ExitReason::Revert(_) => { + listener.otter_transaction_error = format!("0x{}", hex::encode(runtime.machine().return_value())); + } _ => { debug!("Machine: position: {:?}, memory: {:?}, stack: {:?}", runtime.machine().position(), @@ -165,17 +164,21 @@ pub async fn run_evm_impl( &runtime.machine().stack().data().iter().take(128).collect::>()); } } + build_exit_result(executor, &runtime, &backend, &listener, &exit_reason, remaining_gas, is_static, continuations) }, CpsReason::CallInterrupt(i) => { let cont_id = continuations.lock().unwrap().create_continuation(runtime.machine_mut(), executor.state().substate()); + build_call_result(executor, &runtime, &backend, i, &listener, remaining_gas, is_static, cont_id) }, CpsReason::CreateInterrupt(i) => { let cont_id = continuations.lock().unwrap().create_continuation(runtime.machine_mut(), executor.into_state().substate()); + build_create_result(&runtime, i, &listener, remaining_gas, cont_id) } }; + info!( "EVM execution summary: context: {:?}, origin: {:?} address: {:?} gas: {:?} value: {:?}, data: {:?}, extras: {:?}, estimate: {:?}, cps: {:?}, result: {}, returnVal: {}", evm_context, backend.origin, address, gas_limit, apparent_value, @@ -322,6 +325,7 @@ fn build_call_result( context.set_apparent_value(interrupt.context.apparent_value.into()); context.set_caller(interrupt.context.caller.into()); context.set_destination(interrupt.context.address.into()); + trap_data_call.set_context(context); if let Some(tran) = interrupt.transfer { @@ -414,4 +418,4 @@ fn handle_panic(trace: String, remaining_gas: u64, reason: &str) -> EvmProto::Ev result.set_tx_trace(trace.into()); result.set_remaining_gas(remaining_gas); result -} +} \ No newline at end of file diff --git a/evm-ds/src/main.rs b/evm-ds/src/main.rs index 2d12503d0f..5e87d6a05d 100644 --- a/evm-ds/src/main.rs +++ b/evm-ds/src/main.rs @@ -61,6 +61,17 @@ struct Args { zil_scaling_factor: u64, } +#[derive(Debug, Serialize, Deserialize, Default)] +struct OtterscanCallContext { + #[serde(rename = "type")] + pub call_type: String, + pub depth: usize, + pub from: String, + pub to: String, + pub value: String, + pub input: String, +} + #[derive(Debug, Serialize, Deserialize, Default)] struct CallContext { #[serde(rename = "type")] @@ -114,6 +125,10 @@ struct StructLog { struct LoggingEventListener { call_tracer: Vec, raw_tracer: StructLogTopLevel, + otter_internal_tracer: Vec, + otter_call_tracer: Vec, + otter_transaction_error: String, + otter_addresses_called: Vec, enabled: bool, } @@ -126,11 +141,24 @@ struct StructLogTopLevel { pub struct_logs: Vec, } +#[derive(Debug, Serialize, Deserialize, Default)] +struct InternalOperationOtter { + #[serde(rename = "type")] + pub call_type: usize, + pub from: String, + pub to: String, + pub value: String, +} + impl LoggingEventListener { fn new(enabled: bool) -> Self { LoggingEventListener { call_tracer: Default::default(), raw_tracer: Default::default(), + otter_internal_tracer: Default::default(), + otter_call_tracer: Default::default(), + otter_transaction_error: "0x".to_string(), + otter_addresses_called: Default::default(), enabled, } } @@ -142,11 +170,15 @@ impl evm::runtime::tracing::EventListener for LoggingEventListener { return; } + let call_depth = self.call_tracer.len() - 1; + let mut struct_log = StructLog { - depth: self.call_tracer.len() - 1, + depth: call_depth, ..Default::default() }; + let mut intern_trace = None; + match event { evm::runtime::tracing::Event::Step { context: _, @@ -183,11 +215,94 @@ impl evm::runtime::tracing::EventListener for LoggingEventListener { } => { struct_log.op = "SStore".to_string(); } + evm::runtime::tracing::Event::TransactTransfer { + call_type, + address, + target, + balance, + input, + } => { + intern_trace = Some(InternalOperationOtter { + call_type: call_depth, + from: format!("{:?}", address), + to: format!("{:?}", target), + value: format!("{:0X?}", balance), + }); + + self.otter_call_tracer.push(OtterscanCallContext{ + call_type: call_type.to_string(), + depth: call_depth, + from: format!("{:?}", address), + to: format!("{:?}", target), + value: format!("{:0X?}", balance), + input: input.to_string(),}); + + let to_add = format!("{:?}", target); + + // only push if doesn't exist in otter_addresses_called + if !self.otter_addresses_called.contains(&to_add) { + self.otter_addresses_called.push(to_add); + } + } + evm::runtime::tracing::Event::TransactSuicide { + address, + target, + balance, + } => { + intern_trace = Some(InternalOperationOtter { + call_type: 1, + from: format!("{:?}", address), + to: format!("{:?}", target), + value: format!("{:0X?}", balance), + }); + + self.otter_call_tracer.push(OtterscanCallContext{ + call_type: "SELFDESTRUCT".to_string(), + depth: call_depth, + from: format!("{:?}", address), + to: format!("{:?}", target), + value: format!("{:0X?}", balance), + input: "".to_string(),}); + } + evm::runtime::tracing::Event::TransactCreate { + call_type, + address, + target, + balance, + is_create2, + input, + } => { + intern_trace = Some(InternalOperationOtter { + call_type: if is_create2 { 3 } else { 2 }, + from: format!("{:?}", address), + to: format!("{:?}", target), + value: format!("{:0X?}", balance), + }); + + self.otter_call_tracer.push(OtterscanCallContext{ + call_type: call_type.to_string(), + depth: call_depth, + from: format!("{:?}", address), + to: format!("{:?}", target), + value: format!("{:0X?}", balance), + input: input.to_string(),}); + + let to_add = format!("{:?}", target); + + // only push if doesn't exist in otter_addresses_called + if !self.otter_addresses_called.contains(&to_add) { + self.otter_addresses_called.push(to_add); + } + } } if self.raw_tracer.struct_logs.len() < 5 { self.raw_tracer.struct_logs.push(struct_log); } + + if let Some(intern_trace) = intern_trace { + self.otter_internal_tracer.push(intern_trace); + } } } diff --git a/scripts/integration_test_js.sh b/scripts/integration_test_js.sh index 75e4489cb6..b9bb108f9a 100755 --- a/scripts/integration_test_js.sh +++ b/scripts/integration_test_js.sh @@ -118,9 +118,12 @@ else cd tests/EvmAcceptanceTests/ npm install - DEBUG=true MOCHA_TIMEOUT=20000 npx hardhat test + DEBUG=true MOCHA_TIMEOUT=40000 npx hardhat test --bail 2>&1 > npx.out retVal=$? + + pkill -INT isolatedServer + cat npx.out if [ $retVal -ne 0 ]; then echo "!!!!!! Error with JS integration test !!!!!!" exit 1 diff --git a/src/libCps/CpsRunEvm.cpp b/src/libCps/CpsRunEvm.cpp index d182b7876e..e53e4f8d0c 100644 --- a/src/libCps/CpsRunEvm.cpp +++ b/src/libCps/CpsRunEvm.cpp @@ -28,6 +28,7 @@ #include "libEth/utils/EthUtils.h" #include "libMetrics/Api.h" #include "libMetrics/Tracing.h" +#include "libPersistence/BlockStorage.h" #include "libUtils/DataConversion.h" #include "libUtils/EvmUtils.h" #include "libUtils/GasConv.h" @@ -75,6 +76,9 @@ CpsExecuteResult CpsRunEvm::Run(TransactionReceipt& receipt) { TRACE_ERROR("Insufficient Balance"); return {TxnStatus::INSUFFICIENT_BALANCE, false, {}}; } + if (!BlockStorage::GetBlockStorage().PutContractCreator(contractAddress, mCpsContext.scillaExtras.txnHash)) { + LOG_GENERAL(WARNING, "Failed to save contract creator"); + } // Contract call (non-trap) } else if (GetType() == CpsRun::Call) { INC_STATUS(GetCPSMetric(), "transaction", "call"); diff --git a/src/libCps/CpsRunScilla.cpp b/src/libCps/CpsRunScilla.cpp index ab77cd0ee6..b2f0b25f54 100644 --- a/src/libCps/CpsRunScilla.cpp +++ b/src/libCps/CpsRunScilla.cpp @@ -25,6 +25,7 @@ #include "libCps/ScillaHelpersCall.h" #include "libCps/ScillaHelpersCreate.h" #include "libData/AccountData/TransactionReceipt.h" +#include "libPersistence/BlockStorage.h" #include "libScilla/ScillaClient.h" #include "libScilla/ScillaUtils.h" #include "libUtils/DataConversion.h" @@ -224,6 +225,10 @@ CpsExecuteResult CpsRunScilla::runCreate(TransactionReceipt& receipt) { mAccountStore.AddAddressToUpdateBufferAtomic(mArgs.from); mAccountStore.AddAddressToUpdateBufferAtomic(mArgs.dest); + if (!BlockStorage::GetBlockStorage().PutContractCreator(mArgs.dest, mCpsContext.scillaExtras.txnHash)) { + LOG_GENERAL(WARNING, "Failed to save contract creator"); + } + return {TxnStatus::NOT_PRESENT, true, ScillaResult{mArgs.gasLimit}}; } diff --git a/src/libCps/CpsUtils.cpp b/src/libCps/CpsUtils.cpp index 7f40b86f34..e992cd4314 100644 --- a/src/libCps/CpsUtils.cpp +++ b/src/libCps/CpsUtils.cpp @@ -66,7 +66,8 @@ ScillaProcessContext CpsUtils::FromEvmContext( .blockDifficulty = 0, // Not relevant .contractType = Transaction::ContractType::ERROR, + .txnHash = evmContext.GetTranID() }; } -} // namespace libCps \ No newline at end of file +} // namespace libCps diff --git a/src/libData/AccountStore/AccountStore.cpp b/src/libData/AccountStore/AccountStore.cpp index ebbdfc1288..f6d93d6b2c 100644 --- a/src/libData/AccountStore/AccountStore.cpp +++ b/src/libData/AccountStore/AccountStore.cpp @@ -640,8 +640,30 @@ bool AccountStore::UpdateAccountsTemp( << transaction.GetTranID() << "> (" << (status ? "Successfully)" : "Failed)")); - // Record and publish delay + // This needs to be outside the above as needs to include possibility of non evm tx + if(ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + + if (!BlockStorage::GetBlockStorage().PutOtterAddressNonceLookup(transaction.GetTranID(), + transaction.GetNonce() - 1, transaction.GetSenderAddr().hex())) { + LOG_GENERAL(INFO, + "FAIL: Put otter addr mapping failed " << transaction.GetTranID()); + } + // For when vanilla TX, we still want to log this for otterscan + if (!isEvm) { + std::set addresses_touched; + addresses_touched.insert(transaction.GetSenderAddr().hex()); + addresses_touched.insert(transaction.GetToAddr().hex()); + + if (!BlockStorage::GetBlockStorage().PutOtterTxAddressMapping(transaction.GetTranID(), + addresses_touched, blockNum)) { + LOG_GENERAL(INFO, + "FAIL: Put otter addr mapping failed " << transaction.GetTranID()); + } + } + } + + // Record and publish delay auto delay = r_timer_end(tpLatencyStart); double dVal = delay / 1000; if (dVal > 0) { diff --git a/src/libData/AccountStore/AccountStoreSC.cpp b/src/libData/AccountStore/AccountStoreSC.cpp index 9de46a27e5..3388972d5f 100644 --- a/src/libData/AccountStore/AccountStoreSC.cpp +++ b/src/libData/AccountStore/AccountStoreSC.cpp @@ -37,6 +37,7 @@ #include "libCps/CpsExecutor.h" #include "libData/AccountStore/AccountStoreSC.h" #include "libMetrics/Api.h" +#include "libPersistence/BlockStorage.h" namespace { @@ -249,7 +250,9 @@ bool AccountStoreSC::UpdateAccounts( .dsBlockNum = m_curDSBlockNum, .blockTimestamp = extras.block_timestamp, .blockDifficulty = extras.block_difficulty, - .contractType = Transaction::GetTransactionType(transaction)}; + .contractType = Transaction::GetTransactionType(transaction), + .txnHash = transaction.GetTranID() + }; AccountStoreCpsInterface acCpsInterface{*this}; libCps::CpsExecutor cpsExecutor{acCpsInterface, receipt}; @@ -261,6 +264,8 @@ bool AccountStoreSC::UpdateAccounts( cpsRunResult.isSuccess = true; } error_code = cpsRunResult.txnStatus; + + return cpsRunResult.isSuccess; } diff --git a/src/libData/AccountStore/AccountStoreSCEvm.cpp b/src/libData/AccountStore/AccountStoreSCEvm.cpp index b3fc67131c..c05b9d53d0 100644 --- a/src/libData/AccountStore/AccountStoreSCEvm.cpp +++ b/src/libData/AccountStore/AccountStoreSCEvm.cpp @@ -36,6 +36,7 @@ #include "libUtils/Evm.pb.h" #include "libUtils/EvmUtils.h" #include "libUtils/GasConv.h" +#include "libUtils/JsonUtils.h" #include "libUtils/SafeMath.h" #include "libUtils/TimeUtils.h" @@ -283,6 +284,38 @@ bool AccountStoreSC::EvmProcessMessage(EvmProcessContext ¶ms, return status; } +std::string stripTxTraceOut(const std::string &trace) { + + Json::Value trace_json; + JSONUtils::GetInstance().convertStrtoJson(trace, trace_json); + + //Json::Value parsed; + trace_json.removeMember("call_tracer"); + trace_json.removeMember("raw_tracer"); + + return trace_json.toStyledString(); +} + +void getAddressesFromTrace(const std::string &trace, std::set &addresses) { + Json::Value trace_json; + JSONUtils::GetInstance().convertStrtoJson(trace, trace_json); + + Json::Value parsed; + + auto const item = trace_json["otter_addresses_called"]; + parsed = item; + + // non 0x prefixed addresses - strip if present + for (auto const &address : parsed) { + auto addr = address.asString(); + if (addr.substr(0, 2) == "0x") { + addresses.insert(addr); + } else { + addresses.insert(addr.substr(2)); + } + } +} + bool AccountStoreSC::UpdateAccountsEvm(const uint64_t &blockNum, const unsigned int &numShards, const bool &isDS, @@ -338,6 +371,10 @@ bool AccountStoreSC::UpdateAccountsEvm(const uint64_t &blockNum, libCps::CpsExecutor cpsExecutor{acCpsInterface, receipt}; const auto cpsRunResult = cpsExecutor.RunFromEvm(evmContext); + std::set addresses_touched; + addresses_touched.insert(m_originAddr.hex()); + addresses_touched.insert(m_curContractAddr.hex()); + if (std::holds_alternative(cpsRunResult.result) && ARCHIVAL_LOOKUP_WITH_TX_TRACES) { auto const &context = std::get(cpsRunResult.result); @@ -352,6 +389,30 @@ bool AccountStoreSC::UpdateAccountsEvm(const uint64_t &blockNum, LOG_GENERAL(INFO, "FAIL: Put TX trace failed " << evmContext.GetTranID()); } + + // Attempt to parse the addresses called to fulfil ots_searchTransactions* + // The tx has reported all addresses it has touched via call or transfer + // and we can use this to populate the address->tx mapping + getAddressesFromTrace(traces, addresses_touched); + + // we want a version with only otter stuff since we store it + // permanently and the rest is huge + auto const trace_stripped = stripTxTraceOut(traces); + + if (!BlockStorage::GetBlockStorage().PutOtterTrace(evmContext.GetTranID(), + traces)) { + LOG_GENERAL(INFO, + "FAIL: Put otter trace failed " << evmContext.GetTranID()); + } + } + } + + // This needs to be outside the above as needs to include possibility of non evm tx + if(ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + if (!BlockStorage::GetBlockStorage().PutOtterTxAddressMapping(evmContext.GetTranID(), + addresses_touched, blockNum)) { + LOG_GENERAL(INFO, + "FAIL: Put otter addr mapping failed " << evmContext.GetTranID()); } } diff --git a/src/libData/AccountStore/services/scilla/ScillaProcessContext.h b/src/libData/AccountStore/services/scilla/ScillaProcessContext.h index 010704d094..4d818f4ce6 100644 --- a/src/libData/AccountStore/services/scilla/ScillaProcessContext.h +++ b/src/libData/AccountStore/services/scilla/ScillaProcessContext.h @@ -37,6 +37,7 @@ struct ScillaProcessContext { uint128_t blockTimestamp; uint8_t blockDifficulty = 0; Transaction::ContractType contractType; + TxnHash txnHash; }; #endif // ZILLIQA_SRC_LIBDATA_ACCOUNTSTORE_SERVICES_SCILLA_SCILLAPROCESSCONTEXT_H_ diff --git a/src/libMessage/ZilliqaMessage.proto b/src/libMessage/ZilliqaMessage.proto index b6d1076b83..4317f8d2f0 100644 --- a/src/libMessage/ZilliqaMessage.proto +++ b/src/libMessage/ZilliqaMessage.proto @@ -1272,3 +1272,25 @@ message TxTraceStoredDisk uint64 index = 2; string txTrace = 3; } + +message OtterscanTrace +{ + string trace = 1; +} + +// tx address will map to this and contain all tx hashes +// associated with that address +message OtterscanTraceAddressMapping +{ + message TxHashInfo + { + string hash = 1; + uint64 blocknum = 2; + } + repeated TxHashInfo hashes = 1; +} + +message OtterscanAddressNonceLookup +{ + string hash = 1; +} diff --git a/src/libPersistence/BlockStorage.cpp b/src/libPersistence/BlockStorage.cpp index 9b29a8af4b..9dfe0f8252 100644 --- a/src/libPersistence/BlockStorage.cpp +++ b/src/libPersistence/BlockStorage.cpp @@ -70,9 +70,13 @@ void BlockStorage::Initialize(const std::string& path, bool diagnostic) { m_txBodyDBs.emplace_back(std::make_shared("txBodies")); m_txEpochDB = std::make_shared("txEpochs"); m_txTraceDB = std::make_shared("txTraces"); + m_otterTraceDB = std::make_shared("otterTraces"); + m_otterTxAddressMappingDB = std::make_shared("otterTxAddressMappings"); + m_otterAddressNonceLookup = std::make_shared("otterAddressNonceLookup"); m_minerInfoDSCommDB = std::make_shared("minerInfoDSComm"); m_minerInfoShardsDB = std::make_shared("minerInfoShards"); m_extSeedPubKeysDB = std::make_shared("extSeedPubKeys"); + m_contractCreatorDB = std::make_shared("contractCreators"); } m_microBlockDBs.emplace_back(std::make_shared("microBlocks")); } @@ -1648,6 +1652,24 @@ bool BlockStorage::GetMinerInfoShards(const uint64_t& dsBlockNum, return found; } +bool BlockStorage::PutContractCreator(const dev::h160 address, const dev::h256 txnHash) { + if (!m_contractCreatorDB) { + return true; + } + + lock_guard g(m_contractCreatorMutex); + return m_contractCreatorDB->Insert(address.asBytes(), txnHash.asBytes()); +} + +dev::h256 BlockStorage::GetContractCreator(const dev::h160 address) { + if (!m_contractCreatorDB) { + return dev::h256(); + } + + lock_guard g(m_contractCreatorMutex); + return dev::h256(reinterpret_cast(m_contractCreatorDB->Lookup(address.asBytes()).c_str()), dev::h256::ConstructFromPointerType::ConstructFromPointer); +} + bool BlockStorage::ResetDB(DBTYPE type) { LOG_MARKER(); bool ret = false; @@ -2053,3 +2075,274 @@ void BlockStorage::BuildHashToNumberMappingForTxBlocks() { LOG_GENERAL(WARNING, "There was nothing to catchup"); } } + +bool BlockStorage::PutOtterTrace(const dev::h256& key, const std::string& trace) { + if (!ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + LOG_GENERAL( + WARNING, + "This should only be triggered when archival lookup is enabled!."); + return false; + } + + if (!key) { + LOG_GENERAL(WARNING, "Setting with a zero hash is not allowed"); + return false; + } + + ZilliqaMessage::OtterscanTrace toWrite; + toWrite.set_trace(trace); + + const zbytes& keyBytes = key.asBytes(); + + lock_guard g(m_mutexTxBody); + + if (!m_otterTraceDB) { + LOG_GENERAL( + WARNING, + "Attempt to access non initialized DB! Are you in lookup mode? "); + return false; + } + + // Store txn hash and epoch inside txEpochs DB + if (m_otterTraceDB->Insert(key, toWrite.SerializeAsString()) != 0) { + LOG_GENERAL(WARNING, "Tx trace insertion failed. " + << " key=" << key); + return false; + } + + return true; +} + +bool BlockStorage::GetOtterTrace(const dev::h256& key, std::string& trace) { + const zbytes& keyBytes = key.asBytes(); + + lock_guard g(m_mutexTxBody); + + if (!m_otterTraceDB) { + LOG_GENERAL( + WARNING, + "Attempt to access non initialized DB! Are you in lookup mode? "); + return false; + } + + trace = m_otterTraceDB->Lookup(keyBytes); + + if (trace.empty()) { + return false; + } + + ZilliqaMessage::OtterscanTrace otterTrace; + otterTrace.ParseFromString(trace); + trace = otterTrace.trace(); + + return true; +} + +bool BlockStorage::PutOtterTxAddressMapping(const dev::h256& txId, const std::set& addresses, const uint64_t& blocknum) { + if (!ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + LOG_GENERAL( + WARNING, + "This should only be triggered when archival lookup is enabled!."); + return false; + } + + if (!txId) { + LOG_GENERAL(WARNING, "Setting with a zero txid is not allowed"); + return false; + } + + lock_guard g(m_mutexTxBody); + + // for each address, add to the tx hashes and block number that touched them + for (auto address : addresses) { + + // lowercase address + std::transform(address.begin(), address.end(), address.begin(), ::tolower); + + // if address has 0x prefix, remove it + if (address.substr(0,2) == "0x") { + address = address.substr(2); + } + + // if address is all zeroes, do not want it inserted + if (address == "0000000000000000000000000000000000000000") { + continue; + } + + if (address.size() != 40) { + LOG_GENERAL(WARNING, "Address " << address << " is not 40 characters long"); + continue; + } + + ZilliqaMessage::OtterscanTraceAddressMapping ret; + ZilliqaMessage::OtterscanTraceAddressMapping_TxHashInfo internal; + + auto res = m_otterTxAddressMappingDB->Lookup(address); + + if(!res.empty()) { + ret.ParseFromString(res); + } + + // Now add to the internal list + internal.set_hash(txId.hex()); + internal.set_blocknum(blocknum); + ret.mutable_hashes()->Add(std::move(internal)); + + zbytes ser(ret.ByteSizeLong()); + ret.SerializeToArray(ser.data(), ser.size()); + + m_otterTxAddressMappingDB->Insert(address, ser); + } + + return true; +} + +std::vector BlockStorage::GetOtterTxAddressMapping(std::string address, unsigned long blockNumber, unsigned long pageSize, bool before, bool &wasMore) { + + std::vector addresses; + lock_guard g(m_mutexTxBody); + + if (!m_otterTxAddressMappingDB) { + LOG_GENERAL( + WARNING, + "Attempt to access non initialized DB! Are you in lookup mode? "); + return {}; + } + + // lowercase the query + std::transform(address.begin(), address.end(), address.begin(), ::tolower); + + // remove 0x prefix from address if exists + if (address.substr(0,2) == "0x") { + address = address.substr(2); + } + + if (address.size() != 40) { + LOG_GENERAL(WARNING, "Address " << address << " is not 40 characters long"); + return {}; + } + + std::string ret = m_otterTxAddressMappingDB->Lookup(address); + + if (ret.empty()) { + return {}; + } + + ZilliqaMessage::OtterscanTraceAddressMapping otterTxAddressMapping; + otterTxAddressMapping.ParseFromString(ret); + + auto hashes = otterTxAddressMapping.hashes(); + + // If we are searching before the block number, + // we need to reverse the order of the hashes so we are searching downward + if(before) { + std::reverse(hashes.begin(), hashes.end()); + } + + uint64_t stopOnBlock = -1; + + for(int i = 0; i < hashes.size();i++) { + auto item = hashes.Get(i); + + if (item.blocknum() == stopOnBlock) { + wasMore = true; + break; + } + + // The otter docs indicate + if(addresses.size() >= pageSize) { + stopOnBlock = item.blocknum(); + stopOnBlock = before ? stopOnBlock - 1 : stopOnBlock + 1; + } + + if(before) { + if(item.blocknum() < blockNumber) { + addresses.push_back(item.hash()); + } + } else { + if(item.blocknum() > blockNumber) { + addresses.push_back(item.hash()); + } + } + } + + // Results should always be descending, so they need to be reversed + // if the search was 'after' + if(!before) { + std::reverse(addresses.begin(), addresses.end()); + } + + return addresses; +} + + +bool BlockStorage::PutOtterAddressNonceLookup(const dev::h256& txId, uint64_t nonce, std::string address) { + if (!ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + LOG_GENERAL( + WARNING, + "This should only be triggered when archival lookup is enabled!."); + return false; + } + + if (!txId) { + LOG_GENERAL(WARNING, "Setting with a zero txid is not allowed"); + return false; + } + + std::transform(address.begin(), address.end(), address.begin(), ::tolower); + + // remove 0x prefix from address if exists + if (address.substr(0,2) == "0x") { + address = address.substr(2); + } + + if (address.size() != 40) { + LOG_GENERAL(WARNING, "Address " << address << " is not 40 characters long"); + return {}; + } + + // Create lookup key as concatenation of address and nonce + std::string key = address + std::to_string(nonce); + + lock_guard g(m_mutexTxBody); + + ZilliqaMessage::OtterscanAddressNonceLookup insert; + insert.set_hash("0x" + txId.hex()); + + zbytes ser(insert.ByteSizeLong()); + insert.SerializeToArray(ser.data(), ser.size()); + + return m_otterAddressNonceLookup->Insert(key, ser) == 0; +} + +std::string BlockStorage::GetOtterAddressNonceLookup(std::string address, uint64_t nonce) { + + lock_guard g(m_mutexTxBody); + + if (!m_otterAddressNonceLookup) { + LOG_GENERAL( + WARNING, + "Attempt to access non initialized DB! Are you in lookup mode? "); + return {}; + } + + // remove 0x prefix from address if exists, make lowercase + std::transform(address.begin(), address.end(), address.begin(), ::tolower); + + if (address.substr(0,2) == "0x") { + address = address.substr(2); + } + + if (address.size() != 40) { + LOG_GENERAL(WARNING, "Address " << address << " is not 40 characters long"); + return {}; + } + + std::string key = address + std::to_string(nonce); + std::string ret = m_otterAddressNonceLookup->Lookup(key); + + ZilliqaMessage::OtterscanAddressNonceLookup txnId; + txnId.ParseFromString(ret); + + return txnId.hash(); +} diff --git a/src/libPersistence/BlockStorage.h b/src/libPersistence/BlockStorage.h index 6af4454864..11eac08e1d 100644 --- a/src/libPersistence/BlockStorage.h +++ b/src/libPersistence/BlockStorage.h @@ -87,6 +87,9 @@ class BlockStorage : boost::noncopyable { std::shared_ptr m_txBodyOrigDB; std::shared_ptr m_txEpochDB; std::shared_ptr m_txTraceDB; + std::shared_ptr m_otterTraceDB; + std::shared_ptr m_otterTxAddressMappingDB; + std::shared_ptr m_otterAddressNonceLookup; std::vector> m_microBlockDBs; std::shared_ptr m_microBlockOrigDB; std::shared_ptr m_microBlockKeyDB; @@ -108,6 +111,8 @@ class BlockStorage : boost::noncopyable { std::shared_ptr m_minerInfoShardsDB; /// used for extseed pub key storage and retrieval std::shared_ptr m_extSeedPubKeysDB; + /// stores the hash of the transaction which created a contract + std::shared_ptr m_contractCreatorDB; BlockStorage(const std::string& path = "", bool diagnostic = false) : m_diagnosticDBNodesCounter(0), m_diagnosticDBCoinbaseCounter(0) { @@ -202,6 +207,18 @@ class BlockStorage : boost::noncopyable { bool GetTxTrace(const dev::h256& key, std::string& trace); std::shared_ptr GetTxTraceDb(); + /// Retrieves the requested transaction trace for otterscan. + bool PutOtterTrace(const dev::h256& key, const std::string& trace); + bool GetOtterTrace(const dev::h256& key, std::string& trace); + + /// Retrieves the mappings of address touched to TX. + bool PutOtterTxAddressMapping(const dev::h256& txId, const std::set& addresses, const uint64_t& blocknum); + std::vector GetOtterTxAddressMapping(std::string address, unsigned long blockNumber, unsigned long pageSize, bool before, bool &wasMore); + + /// Retrieves the mappings of address touched to TX. + bool PutOtterAddressNonceLookup(const dev::h256& txId, uint64_t nonce, std::string address); + std::string GetOtterAddressNonceLookup(std::string address, uint64_t nonce); + /// Deletes the requested Tx block bool DeleteTxBlock(const uint64_t& blocknum); @@ -325,6 +342,12 @@ class BlockStorage : boost::noncopyable { /// Retrieves the requested miner info (shards) bool GetMinerInfoShards(const uint64_t& dsBlockNum, MinerInfoShards& entry); + /// Put contract creation transaction hash + bool PutContractCreator(const dev::h160 address, const dev::h256 txnHash); + + /// Get a contract creation transaction hash + dev::h256 GetContractCreator(const dev::h160 address); + /// Clean a DB bool ResetDB(DBTYPE type); @@ -356,6 +379,7 @@ class BlockStorage : boost::noncopyable { mutable std::shared_timed_mutex m_mutexMinerInfoDSComm; mutable std::shared_timed_mutex m_mutexMinerInfoShards; mutable std::shared_timed_mutex m_mutexExtSeedPubKeys; + mutable std::mutex m_contractCreatorMutex; unsigned int m_diagnosticDBNodesCounter; unsigned int m_diagnosticDBCoinbaseCounter; diff --git a/src/libServer/EthRpcMethods.cpp b/src/libServer/EthRpcMethods.cpp index 81130a2496..081fa042ff 100644 --- a/src/libServer/EthRpcMethods.cpp +++ b/src/libServer/EthRpcMethods.cpp @@ -40,6 +40,7 @@ #include "libPOW/pow.h" #include "libPersistence/BlockStorage.h" #include "libServer/AddressChecksum.h" +#include "libUtils/CommonUtils.h" #include "libUtils/DataConversion.h" #include "libUtils/Evm.pb.h" #include "libUtils/EvmUtils.h" @@ -373,6 +374,41 @@ void EthRpcMethods::Init(LookupServer *lookupServer) { "param02", jsonrpc::JSON_OBJECT, NULL), &EthRpcMethods::DebugTraceTransactionI); + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_getInternalOperations", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_STRING, "param01", jsonrpc::JSON_STRING, + NULL), + &EthRpcMethods::OtterscanGetInternalOperationsI); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_traceTransaction", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_STRING, "param01", jsonrpc::JSON_STRING, + NULL), + &EthRpcMethods::OtterscanTraceTransactionI); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_searchTransactionsBefore", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, "param02", jsonrpc::JSON_INTEGER, "param03", jsonrpc::JSON_INTEGER, NULL), + &EthRpcMethods::OtterscanSearchTransactionsBeforeI); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_searchTransactionsAfter", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, "param02", jsonrpc::JSON_INTEGER, "param03", jsonrpc::JSON_INTEGER, NULL), + &EthRpcMethods::OtterscanSearchTransactionsAfterI); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_getTransactionBySenderAndNonce", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, "param02", jsonrpc::JSON_INTEGER, NULL), + &EthRpcMethods::OtterscanGetTransactionBySenderAndNonceI); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_getTransactionError", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_STRING, "param01", jsonrpc::JSON_STRING, + NULL), + &EthRpcMethods::OtterscanGetTransactionErrorI); + m_lookupServer->bindAndAddExternalMethod( jsonrpc::Procedure("debug_traceBlockByNumber", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, @@ -384,6 +420,56 @@ void EthRpcMethods::Init(LookupServer *lookupServer) { jsonrpc::Procedure("GetDSLeaderTxnPool", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, nullptr), &EthRpcMethods::GetDSLeaderTxnPoolI); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("erigon_getHeaderByNumber", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetHeaderByNumberI + ); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_getApiLevel", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetOtterscanApiLevelI + ); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_hasCode", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_BOOLEAN, + "param01", jsonrpc::JSON_STRING, + "param02", jsonrpc::JSON_STRING, + NULL), + &EthRpcMethods::HasCodeI + ); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_getBlockDetails", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetBlockDetailsI + ); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_getBlockTransactions", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_INTEGER, + "param02", jsonrpc::JSON_INTEGER, + "param03", jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetBlockTransactionsI + ); + + m_lookupServer->bindAndAddExternalMethod( + jsonrpc::Procedure("ots_getContractCreator", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_STRING, + NULL), + &EthRpcMethods::GetContractCreatorI + ); } std::string EthRpcMethods::CreateTransactionEth( @@ -662,10 +748,19 @@ Json::Value extractTracer(const std::string &tracer, const std::string &trace) { } else if (tracer.compare("raw") == 0) { auto const item = trace_json["raw_tracer"]; parsed = item; + } else if (tracer.compare("otter_internal_tracer") == 0) { + auto const item = trace_json["otter_internal_tracer"]; + parsed = item; + } else if (tracer.compare("otter_call_tracer") == 0) { + auto const item = trace_json["otter_call_tracer"]; + parsed = item; + } else if (tracer.compare("otter_transaction_error") == 0) { + auto const item = trace_json["otter_transaction_error"]; + parsed = item; } else { throw JsonRpcException( ServerBase::RPC_MISC_ERROR, - std::string("Only callTracer and raw are supported. Received: ") + + std::string("Only callTracer, internal_tracer, otter_call_tracer, otter_transaction_error, and raw are supported. Received: ") + tracer); } } catch (exception &e) { @@ -1975,3 +2070,223 @@ Json::Value EthRpcMethods::DebugTraceTransaction(const std::string &txHash, throw JsonRpcException(ServerBase::RPC_MISC_ERROR, "Unable to Process"); } } + + +Json::Value EthRpcMethods::OtterscanSearchTransactions(const std::string& address, unsigned long blockNumber, unsigned long pageSize, bool before) { + if (!ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, + "The node is not configured to store otter internal operations"); + } + + if (!TX_TRACES) { + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, + "The node is not configured to store otter internal operations"); + } + + // if blocnumber is 0 and it's a before search, then we need to get the latest block number + if (blockNumber == 0 && before) { + blockNumber = m_sharedMediator.m_txBlockChain.GetLastBlock().GetHeader().GetBlockNum(); + } + + try { + bool wasMore = false; + const auto res = + BlockStorage::GetBlockStorage().GetOtterTxAddressMapping(address, blockNumber, pageSize, before, wasMore); + + Json::Value response = Json::objectValue; + Json::Value txs = Json::arrayValue; + Json::Value receipts = Json::arrayValue; + + for(const auto& hash : res) { + // Get Tx result + auto const txByHash = GetEthTransactionByHash(hash); + auto txReceipt = GetEthTransactionReceipt(hash); + + // For some reason otterscan expects a timestamp in the receipts... + auto const block = GetEthBlockByNumber(txReceipt["blockNumber"].asString(), false); + txReceipt["timestamp"] = block["timestamp"]; + + txs.append(txByHash); + receipts.append(txReceipt); + } + + response["txs"] = txs; + response["receipts"] = receipts; + + // Otterscan docs. If results are less than pagesize, results returned as-is. + if (!(res.size() < pageSize)) { + response["firstPage"] = before || !wasMore; + response["lastPage"] = !before || !wasMore; + } + + return response; + } catch (exception &e) { + LOG_GENERAL(INFO, "[Error]" << e.what() << ". Input: " << address); + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, "Unable to Process"); + } +} + +Json::Value EthRpcMethods::OtterscanGetTransactionBySenderAndNonce(const std::string& address, uint64_t nonce) { + if (!ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, + "The node is not configured to store otter internal operations"); + } + + if (!TX_TRACES) { + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, + "The node is not configured to store otter internal operations"); + } + + try { + const auto res = + BlockStorage::GetBlockStorage().GetOtterAddressNonceLookup(address, nonce); + + // Perhaps this should just return empty array + if (res.empty()) { + LOG_GENERAL(INFO, "Otterscan addr nonce request failed! "); + return Json::nullValue; + } + + return res; + } catch (exception &e) { + LOG_GENERAL(INFO, "[Error]" << e.what() << ". Input: " << address); + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, "Unable to Process"); + } +} + +Json::Value EthRpcMethods::OtterscanGetInternalOperations(const std::string &txHash, const std::string &tracer) { + if (!ARCHIVAL_LOOKUP_WITH_TX_TRACES) { + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, + "The node is not configured to store otter internal operations"); + } + + if (!TX_TRACES) { + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, + "The node is not configured to store otter internal operations"); + } + + std::string trace; + + try { + TxnHash tranHash(txHash); + + bool isPresent = + BlockStorage::GetBlockStorage().GetOtterTrace(tranHash, trace); + + if (!isPresent) { + LOG_GENERAL(INFO, "Otterscan trace request failed! "); + return Json::nullValue; + } + + return extractTracer(tracer, trace); + } catch (exception &e) { + LOG_GENERAL(INFO, "[Error]" << e.what() << ". Input: " << txHash); + throw JsonRpcException(ServerBase::RPC_MISC_ERROR, "Unable to Process"); + } +} + +Json::Value EthRpcMethods::GetHeaderByNumber(const uint64_t blockNumber) { + // Erigon headers are a subset of a full block - So just return the full block. + return EthRpcMethods::GetEthBlockByNumber(std::to_string(blockNumber), false); +} + +bool EthRpcMethods::HasCode(const std::string& address, const std::string& /*block*/) { + // TODO: Respect block parameter - We can probably do this by finding the contract creation transaction and comparing + // the block numbers. + Address addr{address, Address::FromHex}; + unique_lock lock( + AccountStore::GetInstance().GetPrimaryMutex()); + AccountStore::GetInstance().GetPrimaryWriteAccessCond().wait(lock, [] { + return AccountStore::GetInstance().GetPrimaryWriteAccess(); + }); + + const Account *account = AccountStore::GetInstance().GetAccount(addr, true); + if (account) { + return !account->GetCode().empty(); + } else { + return false; + } +} + +Json::Value EthRpcMethods::GetBlockDetails(const uint64_t blockNumber) { + Json::Value response; + + auto txBlock = m_sharedMediator.m_txBlockChain.GetBlock(blockNumber); + bool isVacuous = CommonUtils::IsVacuousEpoch(txBlock.GetHeader().GetBlockNum()); + uint128_t rewards = (isVacuous ? txBlock.GetHeader().GetRewards() * EVM_ZIL_SCALING_FACTOR : 0); + uint128_t fees = (isVacuous ? 0 : txBlock.GetHeader().GetRewards() * EVM_ZIL_SCALING_FACTOR); + auto jsonBlock = GetEthBlockCommon(txBlock, false); + + jsonBlock["gasLimit"] = "0x1"; + + jsonBlock.removeMember("transactions"); + jsonBlock["transactionCount"] = txBlock.GetHeader().GetNumTxs(); + jsonBlock["logsBloom"] = Json::nullValue; + response["block"] = jsonBlock; + + response["issuance"]["blockReward"] = rewards.str(); + response["issuance"]["uncleReward"] = 0; + response["issuance"]["issuance"] = rewards.str(); + + response["totalFees"] = fees.str(); + return response; +} + +Json::Value EthRpcMethods::GetBlockTransactions(const uint64_t blockNumber, const uint32_t pageNumber, const uint32_t pageSize) { + Json::Value response; + + auto txBlock = m_sharedMediator.m_txBlockChain.GetBlock(blockNumber); + auto jsonBlock = GetEthBlockCommon(txBlock, true); + + auto transactions = jsonBlock["transactions"]; + + auto start = pageNumber * pageSize; + auto end = std::min(transactions.size(), (pageNumber + 1) * pageSize); + + std::vector receipts; + for (Json::Value::ArrayIndex i = start; i < end; i++) { + auto transaction = transactions[i]; + // TODO: Truncate input to 4 bytes (plus 0x) - Work out why the 0x is optional + + auto receipt = EthRpcMethods::GetEthTransactionReceipt(transaction["hash"].asString()); + receipt["logs"] = Json::nullValue; + receipt["logsBloom"] = Json::nullValue; + receipts.push_back(receipt); + } + + response["fullblock"] = jsonBlock; + + for (auto r : receipts) { + response["receipts"].append(r); + } + + return response; +} + +Json::Value EthRpcMethods::GetContractCreator(const std::string& address) { + Address addr{address, Address::FromHex}; + + dev::h256 creationTxn = BlockStorage::GetBlockStorage().GetContractCreator(addr); + + if (creationTxn == dev::h256()) { + return Json::nullValue; + } + + TxBodySharedPtr txnBodyPtr; + bool isPresent = + BlockStorage::GetBlockStorage().GetTxBody(creationTxn, txnBodyPtr); + if (!isPresent) { + LOG_GENERAL(WARNING, "Contract creator transaction doesn't exist"); + return Json::nullValue; + } + const TransactionWithReceipt& txnBody = *txnBodyPtr; + + Json::Value response = Json::objectValue; + + response["hash"] = "0x" + creationTxn.hex(); + // FIXME: This is wrong for deployer contracts. + // "For deployer contracts, i.e., the contract is created as a result of a method call, this corresponds to the address of the contract who created it." + response["creator"] = "0x" + txnBody.GetTransaction().GetSenderAddr().hex(); + + return response; +} diff --git a/src/libServer/EthRpcMethods.h b/src/libServer/EthRpcMethods.h index 3258c5e7b1..38e5f386e6 100644 --- a/src/libServer/EthRpcMethods.h +++ b/src/libServer/EthRpcMethods.h @@ -584,6 +584,72 @@ class EthRpcMethods { response = this->DebugTraceTransaction(request[0u].asString(), request[1u]); } + /** + * @brief Handles json rpc 2.0 request on method: ots_getInternalOperations + * @param request : transaction hash + * @param response : transaction internal operations + */ + inline virtual void OtterscanGetInternalOperationsI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->OtterscanGetInternalOperations(request[0u].asString(), "otter_internal_tracer"); + } + + /** + * @brief Handles json rpc 2.0 request on method: ots_getInternalOperations + * @param request : transaction hash + * @param response : transaction internal operations + */ + inline virtual void OtterscanGetTransactionErrorI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->OtterscanGetInternalOperations(request[0u].asString(), "otter_transaction_error"); + } + + /** + * @brief Handles json rpc 2.0 request on method: ots_traceTransaction + * @param request : transaction hash + * @param response : transaction trace (abridged) + */ + inline virtual void OtterscanTraceTransactionI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->OtterscanGetInternalOperations(request[0u].asString(), "otter_call_tracer"); + } + + /** + * @brief Handles json rpc 2.0 request on method: ots_traceTransaction + * @param request : transaction hash + * @param response : transaction trace (abridged) + */ + inline virtual void OtterscanSearchTransactionsBeforeI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->OtterscanSearchTransactions(request[0u].asString(), request[1u].asInt64(), request[2u].asInt64(), true); + } + + /** + * @brief Handles json rpc 2.0 request on method: ots_traceTransaction + * @param request : transaction hash + * @param response : transaction trace (abridged) + */ + inline virtual void OtterscanSearchTransactionsAfterI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->OtterscanSearchTransactions(request[0u].asString(), request[1u].asInt64(), request[2u].asInt64(), false); + } + + /** + * @brief Handles json rpc 2.0 request on method: ots_traceTransaction + * @param request : transaction hash + * @param response : transaction trace (abridged) + */ + inline virtual void OtterscanGetTransactionBySenderAndNonceI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->OtterscanGetTransactionBySenderAndNonce(request[0u].asString(), request[1u].asInt64()); + } + /** * @brief Handles json rpc 2.0 request on method: debug_traceBlockByNumber * @param request : block number, trace type @@ -596,6 +662,40 @@ class EthRpcMethods { this->DebugTraceBlockByNumber(request[0u].asString(), request[1u]); } + inline virtual void GetHeaderByNumberI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->GetHeaderByNumber(request[0u].asUInt64()); + } + + inline virtual void GetOtterscanApiLevelI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = 8; + } + + inline virtual void HasCodeI(const Json::Value& request, Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->HasCode(request[0u].asString(), request[1u].asString()); + } + + inline virtual void GetBlockDetailsI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->GetBlockDetails(request[0u].asUInt64()); + } + + inline virtual void GetBlockTransactionsI(const Json::Value& request, + Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->GetBlockTransactions(request[0u].asUInt64(), boost::numeric_cast(request[1u].asUInt64()), boost::numeric_cast(request[2u].asUInt64())); + } + + inline virtual void GetContractCreatorI(const Json::Value& request, Json::Value& response) { + LOG_MARKER_CONTITIONAL(LOG_SC); + response = this->GetContractCreator(request[0u].asString()); + } + struct ApiKeys; std::string GetEthCallZil(const Json::Value& _json); std::string GetEthCallEth(const Json::Value& _json, @@ -674,9 +774,18 @@ class EthRpcMethods { Json::Value GetEthBlockReceipts(const std::string& blockId); Json::Value DebugTraceTransaction(const std::string& txHash, const Json::Value& json); + Json::Value OtterscanGetInternalOperations(const std::string& txHash, const std::string &tracer); + Json::Value OtterscanSearchTransactions(const std::string& address, unsigned long blockNumber, unsigned long pageSize, bool before); + Json::Value OtterscanGetTransactionBySenderAndNonce(const std::string& address, uint64_t nonce); Json::Value DebugTraceBlockByNumber(const std::string& blockNum, const Json::Value& json); + Json::Value GetHeaderByNumber(const uint64_t blockNumber); + bool HasCode(const std::string& address, const std::string& block); + Json::Value GetBlockDetails(const uint64_t blockNumber); + Json::Value GetBlockTransactions(const uint64_t blockNumber, const uint32_t pageNumber, const uint32_t pageSize); + Json::Value GetContractCreator(const std::string& address); + Json::Value GetDSLeaderTxnPool(); void EnsureEvmAndLookupEnabled(); static bool UnpackRevert(const std::string &data_in, std::string &message); diff --git a/src/libServer/IsolatedServer.cpp b/src/libServer/IsolatedServer.cpp index 4d3264fada..f1af9aedb0 100644 --- a/src/libServer/IsolatedServer.cpp +++ b/src/libServer/IsolatedServer.cpp @@ -459,12 +459,98 @@ void IsolatedServer::BindAllEvmMethods() { jsonrpc::JSON_OBJECT, NULL), &LookupServer::DebugTraceTransactionI); + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_getInternalOperations", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, NULL), + &LookupServer::OtterscanGetInternalOperationsI); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_getTransactionBySenderAndNonce", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, "param02", jsonrpc::JSON_INTEGER, NULL), + &LookupServer::OtterscanGetTransactionBySenderAndNonceI); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_getTransactionError", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, NULL), + &LookupServer::OtterscanGetTransactionErrorI); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_searchTransactionsBefore", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, "param02", jsonrpc::JSON_INTEGER, "param03", jsonrpc::JSON_INTEGER, NULL), + &LookupServer::OtterscanSearchTransactionsBeforeI); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_searchTransactionsAfter", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, "param02", jsonrpc::JSON_INTEGER, "param03", jsonrpc::JSON_INTEGER, NULL), + &LookupServer::OtterscanSearchTransactionsAfterI); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_traceTransaction", + jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, + "param01", jsonrpc::JSON_STRING, NULL), + &LookupServer::OtterscanTraceTransactionI); + AbstractServer::bindAndAddMethod( jsonrpc::Procedure("debug_traceBlockByNumber", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_STRING, "param01", jsonrpc::JSON_STRING, "param02", jsonrpc::JSON_OBJECT, NULL), &LookupServer::DebugTraceBlockByNumberI); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("erigon_getHeaderByNumber", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetHeaderByNumberI + ); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_getApiLevel", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetOtterscanApiLevelI + ); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_hasCode", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_BOOLEAN, + "param01", jsonrpc::JSON_STRING, + "param02", jsonrpc::JSON_STRING, + NULL), + &EthRpcMethods::HasCodeI + ); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_getBlockDetails", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetBlockDetailsI + ); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_getBlockTransactions", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_INTEGER, + "param02", jsonrpc::JSON_INTEGER, + "param03", jsonrpc::JSON_INTEGER, + NULL), + &EthRpcMethods::GetBlockTransactionsI + ); + + AbstractServer::bindAndAddMethod( + jsonrpc::Procedure("ots_getContractCreator", jsonrpc::PARAMS_BY_POSITION, + jsonrpc::JSON_OBJECT, + "param01", jsonrpc::JSON_STRING, + NULL), + &EthRpcMethods::GetContractCreatorI + ); } } diff --git a/tests/EvmAcceptanceTests/contracts/Revert.sol b/tests/EvmAcceptanceTests/contracts/Revert.sol index 9501ed984b..aa3e95e8ae 100644 --- a/tests/EvmAcceptanceTests/contracts/Revert.sol +++ b/tests/EvmAcceptanceTests/contracts/Revert.sol @@ -31,6 +31,10 @@ contract Revert { revert(revertMessage); } + function requireCustom(bool success, string memory revertMessage) public payable { + require(success == true, revertMessage); + } + function revertCallWithCustomError() public payable { revert FakeError({value: msg.value, sender: msg.sender}); } diff --git a/tests/EvmAcceptanceTests/test/OtterTests.ts b/tests/EvmAcceptanceTests/test/OtterTests.ts new file mode 100644 index 0000000000..e2b2d121b5 --- /dev/null +++ b/tests/EvmAcceptanceTests/test/OtterTests.ts @@ -0,0 +1,185 @@ +import {assert, expect} from "chai"; +import {ethers} from "hardhat"; +import {expectRevert} from "@openzeppelin/test-helpers"; +import sendJsonRpcRequest from "../helpers/JsonRpcHelper"; +import {parallelizer} from "../helpers"; + +describe("Otterscan api tests", function () { + + it("When we revert the TX, we can get the tx error ", async function () { + const METHOD = "ots_getTransactionError"; + const REVERT_MESSAGE = "Transaction too old"; + + const abi = ethers.utils.defaultAbiCoder; + const MESSAGE_ENCODED = "0x08c379a0" + abi.encode(["string"], [REVERT_MESSAGE]).split('x')[1]; + + const Contract = await ethers.getContractFactory("Revert"); + this.contract = await Contract.deploy(); + + // In order to make a tx that fails at runtime and not estimate gas time, we estimate the gas of + // a similar passing call and use this (+30% leeway) to override the gas field + const estimatedGas = await this.contract.estimateGas.requireCustom(true, REVERT_MESSAGE); + + console.log("Estimated gas: ", estimatedGas); + + const tx = await this.contract.requireCustom(false, REVERT_MESSAGE, { gasLimit: estimatedGas.mul(130).div(100) }) + + await sendJsonRpcRequest(METHOD, 1, [tx.hash], (result, status) => { + assert.equal(status, 200, "has status code"); + + let jsonObject = result.result; + + assert.equal(jsonObject, MESSAGE_ENCODED); + }); + }); + + it("We can get the otter internal operations", async function () { + const METHOD = "ots_getInternalOperations"; + + // Check we can for example detect a suicide with correct value. + // Below is taken from transfer.ts test + const ACCOUNTS_COUNT = 3; + const ACCOUNT_VALUE = 123_000_000; + + const accounts = Array.from({length: ACCOUNTS_COUNT}, (v, k) => + ethers.Wallet.createRandom().connect(ethers.provider) + ); + + const addresses = accounts.map((signer) => signer.address); + + const tx = await parallelizer.deployContract("BatchTransferCtor", addresses, ACCOUNT_VALUE, { + value: ACCOUNTS_COUNT * ACCOUNT_VALUE + }); + + + const balances = await Promise.all(accounts.map((account) => account.getBalance())); + balances.forEach((el) => expect(el).to.be.eq(ACCOUNT_VALUE)); + + await sendJsonRpcRequest(METHOD, 1, [tx.deployTransaction.hash], (result, status) => { + assert.equal(status, 200, "has status code"); + + let jsonObject = result.result; + + assert.equal(jsonObject[0]["type"], 0, "has correct type for transfer"); + assert.equal(jsonObject[3]["type"], 1, "has correct type for self destruct"); + }); + + }); + + it("We can get the otter trace transaction", async function () { + const METHOD = "ots_traceTransaction"; + + let contractOne = await parallelizer.deployContract("ContractOne"); + let contractTwo = await parallelizer.deployContract("ContractTwo"); + let contractThree = await parallelizer.deployContract("ContractThree"); + + let addrOne = contractOne.address.toLowerCase(); + let addrTwo = contractTwo.address.toLowerCase(); + let addrThree = contractThree.address.toLowerCase(); + + let res = await contractOne.chainedCall([addrTwo, addrThree, addrOne], 0); + + await sendJsonRpcRequest(METHOD, 1, [res.hash], (result, status) => { + assert.equal(status, 200, "has status code"); + + let jsonObject = result.result; + + assert.equal(jsonObject[0]["depth"], 0, "has correct depth initially"); + assert.equal(jsonObject[1]["depth"], 1, "has correct depth one call down"); + assert.equal(jsonObject[1]["type"], "CALL", "has correct depth one call down"); + }); + + }); + + it("We can get the otter search for sender by nonce", async function () { + const METHOD = "ots_getTransactionBySenderAndNonce"; + + // To test this, send money to an account then have it send it back. + // The nonces should be able to lookup via 0, 1, 2 + // re-use the batch transfer code for this + const ACCOUNTS_COUNT = 1; + const ACCOUNT_VALUE = 100_000_000; + + const accounts = Array.from({length: ACCOUNTS_COUNT}, (v, k) => + ethers.Wallet.createRandom().connect(ethers.provider) + ); + + const acctAddr = accounts[0].address; + + const [owner] = await ethers.getSigners(); + let txRawFromOwner = { + to: acctAddr, + value: ethers.utils.parseEther("1") + } + await owner.sendTransaction(txRawFromOwner); + + // Create a transaction object + let txRaw = { + to: owner.address, + value: ethers.utils.parseEther("0.45") + } + const txid0 = await accounts[0].sendTransaction(txRaw); + const txid1 = await accounts[0].sendTransaction(txRaw); + + await sendJsonRpcRequest(METHOD, 1, [acctAddr, 0], (result, status) => { + assert.equal(status, 200, "has status code"); + + let jsonObject = result.result; + + assert.equal(jsonObject, txid0.hash, "has correct hash"); + }); + + await sendJsonRpcRequest(METHOD, 1, [acctAddr, 1], (result, status) => { + assert.equal(status, 200, "has status code"); + + let jsonObject = result.result; + + assert.equal(jsonObject, txid1.hash, "has correct hash"); + }); + + }); + + it("We can get the otter search TX before and after", async function () { + const METHOD_BEFORE = "ots_searchTransactionsBefore"; + const METHOD_AFTER = "ots_searchTransactionsAfter"; + + // run the contract that batch sends funds to other addresses + // then we can check that this txid comes up when asking about + // these contract addresses. + const ACCOUNTS_COUNT = 3; + const ACCOUNT_VALUE = 123_000_000; + + // Get the block height so we can check before/after + const height = await ethers.provider.getBlockNumber(); + + const accounts = Array.from({length: ACCOUNTS_COUNT}, (v, k) => + ethers.Wallet.createRandom().connect(ethers.provider) + ); + + const addresses = accounts.map((signer) => signer.address); + + const tx = await parallelizer.deployContract("BatchTransferCtor", addresses, ACCOUNT_VALUE, { + value: ACCOUNTS_COUNT * ACCOUNT_VALUE + }); + + const balances = await Promise.all(accounts.map((account) => account.getBalance())); + balances.forEach((el) => expect(el).to.be.eq(ACCOUNT_VALUE)); + + await sendJsonRpcRequest(METHOD_AFTER, 1, [addresses[0], height, 100], (result, status) => { + assert.equal(status, 200, "has status code"); + + let jsonObject = result.result; + assert.equal(jsonObject.txs[0].hash, tx.deployTransaction.hash, "Can find the TX which send funds to this addr"); + }); + + // There should be nothing before this point + await sendJsonRpcRequest(METHOD_BEFORE, 1, [addresses[0], height, 100], (result, status) => { + assert.equal(status, 200, "has status code"); + + let jsonObject = result.result; + assert.equal(jsonObject.txs.length, 0, "Can not find the TX which send funds to this addr"); + }); + + }); + +});