You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

815 lines
22 KiB

/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file Block.cpp
* @author Gav Wood <i@gavwood.com>
* @date 2014
*/
#include "Block.h"
#include <ctime>
#include <boost/filesystem.hpp>
#include <boost/timer.hpp>
#include <libdevcore/CommonIO.h>
#include <libdevcore/Assertions.h>
#include <libdevcore/StructuredLogger.h>
#include <libdevcore/TrieHash.h>
#include <libevmcore/Instruction.h>
#include <libethcore/Exceptions.h>
#include <libethcore/Params.h>
#include <libevm/VMFactory.h>
#include "BlockChain.h"
#include "Defaults.h"
#include "ExtVM.h"
#include "Executive.h"
#include "CachedAddressState.h"
#include "CanonBlockChain.h"
#include "TransactionQueue.h"
using namespace std;
using namespace dev;
using namespace dev::eth;
namespace fs = boost::filesystem;
#define ctrace clog(BlockTrace)
#define ETH_TIMED_ENACTMENTS 0
static const unsigned c_maxSyncTransactions = 256;
const char* BlockSafeExceptions::name() { return EthViolet "" EthBlue ""; }
const char* BlockDetail::name() { return EthViolet "" EthWhite ""; }
const char* BlockTrace::name() { return EthViolet "" EthGray ""; }
const char* BlockChat::name() { return EthViolet "" EthWhite ""; }
Block::Block(OverlayDB const& _db, BaseState _bs, Address _coinbaseAddress):
m_state(_db, _bs),
m_beneficiary(_coinbaseAddress),
m_blockReward(c_blockReward)
{
m_previousBlock.clear();
m_currentBlock.clear();
// assert(m_state.root() == m_previousBlock.stateRoot());
}
Block::Block(Block const& _s):
m_state(_s.m_state),
m_transactions(_s.m_transactions),
m_receipts(_s.m_receipts),
m_transactionSet(_s.m_transactionSet),
m_previousBlock(_s.m_previousBlock),
m_currentBlock(_s.m_currentBlock),
m_beneficiary(_s.m_beneficiary),
m_blockReward(_s.m_blockReward)
{
m_precommit = m_state;
m_committedToMine = false;
}
Block& Block::operator=(Block const& _s)
{
if (&_s == this)
return *this;
m_state = _s.m_state;
m_transactions = _s.m_transactions;
m_receipts = _s.m_receipts;
m_transactionSet = _s.m_transactionSet;
m_previousBlock = _s.m_previousBlock;
m_currentBlock = _s.m_currentBlock;
m_beneficiary = _s.m_beneficiary;
m_blockReward = _s.m_blockReward;
m_precommit = m_state;
m_committedToMine = false;
return *this;
}
void Block::resetCurrent()
{
m_transactions.clear();
m_receipts.clear();
m_transactionSet.clear();
m_currentBlock = BlockInfo();
m_currentBlock.setCoinbaseAddress(m_beneficiary);
m_currentBlock.setTimestamp(max(m_previousBlock.timestamp() + 1, (u256)time(0)));
m_currentBlock.populateFromParent(m_previousBlock);
// TODO: check.
m_state.setRoot(m_previousBlock.stateRoot());
m_precommit = m_state;
m_committedToMine = false;
}
PopulationStatistics Block::populateFromChain(BlockChain const& _bc, h256 const& _h, ImportRequirements::value _ir)
{
PopulationStatistics ret { 0.0, 0.0 };
if (!_bc.isKnown(_h))
{
// Might be worth throwing here.
cwarn << "Invalid block given for state population: " << _h;
BOOST_THROW_EXCEPTION(BlockNotFound() << errinfo_target(_h));
}
auto b = _bc.block(_h);
BlockInfo bi(b);
if (bi.number())
{
// Non-genesis:
// 1. Start at parent's end state (state root).
BlockInfo bip(_bc.block(bi.parentHash()));
sync(_bc, bi.parentHash(), bip);
// 2. Enact the block's transactions onto this state.
m_beneficiary = bi.beneficiary();
Timer t;
auto vb = _bc.verifyBlock(&b, function<void(Exception&)>(), _ir | ImportRequirements::TransactionBasic);
ret.verify = t.elapsed();
t.restart();
enact(vb, _bc);
ret.enact = t.elapsed();
}
else
{
// Genesis required:
// We know there are no transactions, so just populate directly.
m_state = State(m_state.db(), BaseState::Empty); // TODO: try with PreExisting.
sync(_bc, _h, bi);
}
return ret;
}
bool Block::sync(BlockChain const& _bc)
{
return sync(_bc, _bc.currentHash());
}
bool Block::sync(BlockChain const& _bc, h256 const& _block, BlockInfo const& _bi)
{
bool ret = false;
// BLOCK
BlockInfo bi = _bi ? _bi : _bc.info(_block);
#if ETH_PARANOIA
if (!bi)
while (1)
{
try
{
auto b = _bc.block(_block);
bi.populate(b);
break;
}
catch (Exception const& _e)
{
// TODO: Slightly nicer handling? :-)
cerr << "ERROR: Corrupt block-chain! Delete your block-chain DB and restart." << endl;
cerr << diagnostic_information(_e) << endl;
}
catch (std::exception const& _e)
{
// TODO: Slightly nicer handling? :-)
cerr << "ERROR: Corrupt block-chain! Delete your block-chain DB and restart." << endl;
cerr << _e.what() << endl;
}
}
#endif
if (bi == m_currentBlock)
{
// We mined the last block.
// Our state is good - we just need to move on to next.
m_previousBlock = m_currentBlock;
resetCurrent();
ret = true;
}
else if (bi == m_previousBlock)
{
// No change since last sync.
// Carry on as we were.
}
else
{
// New blocks available, or we've switched to a different branch. All change.
// Find most recent state dump and replay what's left.
// (Most recent state dump might end up being genesis.)
if (m_state.db().lookup(bi.stateRoot()).empty()) // TODO: API in State for this?
{
cwarn << "Unable to sync to" << bi.hash() << "; state root" << bi.stateRoot() << "not found in database.";
cwarn << "Database corrupt: contains block without stateRoot:" << bi;
cwarn << "Try rescuing the database by running: eth --rescue";
BOOST_THROW_EXCEPTION(InvalidStateRoot() << errinfo_target(bi.stateRoot()));
}
m_previousBlock = bi;
resetCurrent();
ret = true;
}
#if ALLOW_REBUILD
else
{
// New blocks available, or we've switched to a different branch. All change.
// Find most recent state dump and replay what's left.
// (Most recent state dump might end up being genesis.)
std::vector<h256> chain;
while (bi.number() != 0 && m_db.lookup(bi.stateRoot()).empty()) // while we don't have the state root of the latest block...
{
chain.push_back(bi.hash()); // push back for later replay.
bi.populate(_bc.block(bi.parentHash())); // move to parent.
}
m_previousBlock = bi;
resetCurrent();
// Iterate through in reverse, playing back each of the blocks.
try
{
for (auto it = chain.rbegin(); it != chain.rend(); ++it)
{
auto b = _bc.block(*it);
enact(&b, _bc, _ir);
cleanup(true);
}
}
catch (...)
{
// TODO: Slightly nicer handling? :-)
cerr << "ERROR: Corrupt block-chain! Delete your block-chain DB and restart." << endl;
cerr << boost::current_exception_diagnostic_information() << endl;
exit(1);
}
resetCurrent();
ret = true;
}
#endif
return ret;
}
pair<TransactionReceipts, bool> Block::sync(BlockChain const& _bc, TransactionQueue& _tq, GasPricer const& _gp, unsigned msTimeout)
{
// TRANSACTIONS
pair<TransactionReceipts, bool> ret;
ret.second = false;
auto ts = _tq.topTransactions(c_maxSyncTransactions);
LastHashes lh;
auto deadline = chrono::steady_clock::now() + chrono::milliseconds(msTimeout);
for (int goodTxs = 1; goodTxs; )
{
goodTxs = 0;
for (auto const& t: ts)
if (!m_transactionSet.count(t.sha3()))
{
try
{
if (t.gasPrice() >= _gp.ask(*this))
{
// Timer t;
if (lh.empty())
lh = _bc.lastHashes();
execute(lh, t);
ret.first.push_back(m_receipts.back());
++goodTxs;
// cnote << "TX took:" << t.elapsed() * 1000;
}
else if (t.gasPrice() < _gp.ask(*this) * 9 / 10)
{
clog(StateTrace) << t.sha3() << "Dropping El Cheapo transaction (<90% of ask price)";
_tq.drop(t.sha3());
}
}
catch (InvalidNonce const& in)
{
bigint const& req = *boost::get_error_info<errinfo_required>(in);
bigint const& got = *boost::get_error_info<errinfo_got>(in);
if (req > got)
{
// too old
clog(StateTrace) << t.sha3() << "Dropping old transaction (nonce too low)";
_tq.drop(t.sha3());
}
else if (got > req + _tq.waiting(t.sender()))
{
// too new
clog(StateTrace) << t.sha3() << "Dropping new transaction (too many nonces ahead)";
_tq.drop(t.sha3());
}
else
_tq.setFuture(t.sha3());
}
catch (BlockGasLimitReached const& e)
{
bigint const& got = *boost::get_error_info<errinfo_got>(e);
if (got > m_currentBlock.gasLimit())
{
clog(StateTrace) << t.sha3() << "Dropping over-gassy transaction (gas > block's gas limit)";
_tq.drop(t.sha3());
}
else
{
// Temporarily no gas left in current block.
// OPTIMISE: could note this and then we don't evaluate until a block that does have the gas left.
// for now, just leave alone.
}
}
catch (Exception const& _e)
{
// Something else went wrong - drop it.
clog(StateTrace) << t.sha3() << "Dropping invalid transaction:" << diagnostic_information(_e);
_tq.drop(t.sha3());
}
catch (std::exception const&)
{
// Something else went wrong - drop it.
_tq.drop(t.sha3());
cwarn << t.sha3() << "Transaction caused low-level exception :(";
}
}
if (chrono::steady_clock::now() > deadline)
{
ret.second = true;
break;
}
}
return ret;
}
u256 Block::enactOn(VerifiedBlockRef const& _block, BlockChain const& _bc)
{
#if ETH_TIMED_ENACTMENTS
Timer t;
double populateVerify;
double populateGrand;
double syncReset;
double enactment;
#endif
// Check family:
BlockInfo biParent = _bc.info(_block.info.parentHash());
_block.info.verifyParent(biParent);
#if ETH_TIMED_ENACTMENTS
populateVerify = t.elapsed();
t.restart();
#endif
BlockInfo biGrandParent;
if (biParent.number())
biGrandParent = _bc.info(biParent.parentHash());
#if ETH_TIMED_ENACTMENTS
populateGrand = t.elapsed();
t.restart();
#endif
sync(_bc, _block.info.parentHash(), BlockInfo());
resetCurrent();
#if ETH_TIMED_ENACTMENTS
syncReset = t.elapsed();
t.restart();
#endif
m_previousBlock = biParent;
auto ret = enact(_block, _bc);
#if ETH_TIMED_ENACTMENTS
enactment = t.elapsed();
if (populateVerify + populateGrand + syncReset + enactment > 0.5)
clog(StateChat) << "popVer/popGrand/syncReset/enactment = " << populateVerify << "/" << populateGrand << "/" << syncReset << "/" << enactment;
#endif
return ret;
}
u256 Block::enact(VerifiedBlockRef const& _block, BlockChain const& _bc)
{
DEV_TIMED_FUNCTION_ABOVE(500);
// m_currentBlock is assumed to be prepopulated and reset.
#if !ETH_RELEASE
assert(m_previousBlock.hash() == _block.info.parentHash());
assert(m_currentBlock.parentHash() == _block.info.parentHash());
assert(rootHash() == m_previousBlock.stateRoot());
#endif
if (m_currentBlock.parentHash() != m_previousBlock.hash())
// Internal client error.
BOOST_THROW_EXCEPTION(InvalidParentHash());
// Populate m_currentBlock with the correct values.
m_currentBlock.noteDirty();
m_currentBlock = _block.info;
// cnote << "playback begins:" << m_state.root();
// cnote << m_state;
LastHashes lh;
DEV_TIMED_ABOVE("lastHashes", 500)
lh = _bc.lastHashes((unsigned)m_previousBlock.number());
RLP rlp(_block.block);
vector<bytes> receipts;
// All ok with the block generally. Play back the transactions now...
unsigned i = 0;
DEV_TIMED_ABOVE("txExec", 500)
for (auto const& tr: _block.transactions)
{
try
{
LogOverride<ExecutiveWarnChannel> o(false);
execute(lh, tr);
}
catch (Exception& ex)
{
ex << errinfo_transactionIndex(i);
throw;
}
RLPStream receiptRLP;
m_receipts.back().streamRLP(receiptRLP);
receipts.push_back(receiptRLP.out());
++i;
}
h256 receiptsRoot;
DEV_TIMED_ABOVE(".receiptsRoot()", 500)
receiptsRoot = orderedTrieRoot(receipts);
if (receiptsRoot != m_currentBlock.receiptsRoot())
{
InvalidReceiptsStateRoot ex;
ex << Hash256RequirementError(receiptsRoot, m_currentBlock.receiptsRoot());
ex << errinfo_receipts(receipts);
// ex << errinfo_vmtrace(vmTrace(_block.block, _bc, ImportRequirements::None));
BOOST_THROW_EXCEPTION(ex);
}
if (m_currentBlock.logBloom() != logBloom())
{
InvalidLogBloom ex;
ex << LogBloomRequirementError(logBloom(), m_currentBlock.logBloom());
ex << errinfo_receipts(receipts);
BOOST_THROW_EXCEPTION(ex);
}
// Initialise total difficulty calculation.
u256 tdIncrease = m_currentBlock.difficulty();
// Check uncles & apply their rewards to state.
if (rlp[2].itemCount() > 2)
{
TooManyUncles ex;
ex << errinfo_max(2);
ex << errinfo_got(rlp[2].itemCount());
BOOST_THROW_EXCEPTION(ex);
}
vector<BlockInfo> rewarded;
h256Hash excluded;
DEV_TIMED_ABOVE("allKin", 500)
excluded = _bc.allKinFrom(m_currentBlock.parentHash(), 6);
excluded.insert(m_currentBlock.hash());
unsigned ii = 0;
DEV_TIMED_ABOVE("uncleCheck", 500)
for (auto const& i: rlp[2])
{
try
{
auto h = sha3(i.data());
if (excluded.count(h))
{
UncleInChain ex;
ex << errinfo_comment("Uncle in block already mentioned");
ex << errinfo_unclesExcluded(excluded);
ex << errinfo_hash256(sha3(i.data()));
BOOST_THROW_EXCEPTION(ex);
}
excluded.insert(h);
// IgnoreSeal since it's a VerifiedBlock.
BlockInfo uncle(i.data(), IgnoreSeal, h, HeaderData);
BlockInfo uncleParent;
if (!_bc.isKnown(uncle.parentHash()))
BOOST_THROW_EXCEPTION(UnknownParent());
uncleParent = BlockInfo(_bc.block(uncle.parentHash()));
if ((bigint)uncleParent.number() < (bigint)m_currentBlock.number() - 7)
{
UncleTooOld ex;
ex << errinfo_uncleNumber(uncle.number());
ex << errinfo_currentNumber(m_currentBlock.number());
BOOST_THROW_EXCEPTION(ex);
}
else if (uncle.number() == m_currentBlock.number())
{
UncleIsBrother ex;
ex << errinfo_uncleNumber(uncle.number());
ex << errinfo_currentNumber(m_currentBlock.number());
BOOST_THROW_EXCEPTION(ex);
}
uncle.verifyParent(uncleParent);
rewarded.push_back(uncle);
++ii;
}
catch (Exception& ex)
{
ex << errinfo_uncleIndex(ii);
throw;
}
}
DEV_TIMED_ABOVE("applyRewards", 500)
applyRewards(rewarded);
// Commit all cached state changes to the state trie.
DEV_TIMED_ABOVE("commit", 500)
m_state.commit();
// Hash the state trie and check against the state_root hash in m_currentBlock.
if (m_currentBlock.stateRoot() != m_previousBlock.stateRoot() && m_currentBlock.stateRoot() != rootHash())
{
auto r = rootHash();
m_state.db().rollback(); // TODO: API in State for this?
BOOST_THROW_EXCEPTION(InvalidStateRoot() << Hash256RequirementError(r, m_currentBlock.stateRoot()));
}
if (m_currentBlock.gasUsed() != gasUsed())
{
// Rollback the trie.
m_state.db().rollback(); // TODO: API in State for this?
BOOST_THROW_EXCEPTION(InvalidGasUsed() << RequirementError(bigint(gasUsed()), bigint(m_currentBlock.gasUsed())));
}
return tdIncrease;
}
ExecutionResult Block::execute(LastHashes const& _lh, Transaction const& _t, Permanence _p, OnOpFunc const& _onOp)
{
// Uncommitting is a non-trivial operation - only do it once we've verified as much of the
// transaction as possible.
uncommitToMine();
std::pair<ExecutionResult, TransactionReceipt> resultReceipt = m_state.execute(EnvInfo(info(), _lh, gasUsed()), _t, _p, _onOp);
if (_p == Permanence::Committed)
{
// Add to the user-originated transactions that we've executed.
m_transactions.push_back(_t);
m_receipts.push_back(resultReceipt.second);
m_transactionSet.insert(_t.sha3());
}
return resultReceipt.first;
}
void Block::applyRewards(vector<BlockInfo> const& _uncleBlockHeaders)
{
u256 r = m_blockReward;
for (auto const& i: _uncleBlockHeaders)
{
m_state.addBalance(i.beneficiary(), m_blockReward * (8 + i.number() - m_currentBlock.number()) / 8);
r += m_blockReward / 32;
}
m_state.addBalance(m_currentBlock.beneficiary(), r);
}
void Block::commitToSeal(BlockChain const& _bc, bytes const& _extraData)
{
if (m_committedToMine)
uncommitToMine();
else
m_precommit = m_state;
vector<BlockInfo> uncleBlockHeaders;
RLPStream unclesData;
unsigned unclesCount = 0;
if (m_previousBlock.number() != 0)
{
// Find great-uncles (or second-cousins or whatever they are) - children of great-grandparents, great-great-grandparents... that were not already uncles in previous generations.
clog(StateDetail) << "Checking " << m_previousBlock.hash() << ", parent=" << m_previousBlock.parentHash();
h256Hash excluded = _bc.allKinFrom(m_currentBlock.parentHash(), 6);
auto p = m_previousBlock.parentHash();
for (unsigned gen = 0; gen < 6 && p != _bc.genesisHash() && unclesCount < 2; ++gen, p = _bc.details(p).parent)
{
auto us = _bc.details(p).children;
assert(us.size() >= 1); // must be at least 1 child of our grandparent - it's our own parent!
for (auto const& u: us)
if (!excluded.count(u)) // ignore any uncles/mainline blocks that we know about.
{
uncleBlockHeaders.push_back(_bc.info(u));
unclesData.appendRaw(_bc.headerData(u));
++unclesCount;
if (unclesCount == 2)
break;
}
}
}
BytesMap transactionsMap;
BytesMap receiptsMap;
RLPStream txs;
txs.appendList(m_transactions.size());
for (unsigned i = 0; i < m_transactions.size(); ++i)
{
RLPStream k;
k << i;
RLPStream receiptrlp;
m_receipts[i].streamRLP(receiptrlp);
receiptsMap.insert(std::make_pair(k.out(), receiptrlp.out()));
RLPStream txrlp;
m_transactions[i].streamRLP(txrlp);
transactionsMap.insert(std::make_pair(k.out(), txrlp.out()));
txs.appendRaw(txrlp.out());
}
txs.swapOut(m_currentTxs);
RLPStream(unclesCount).appendRaw(unclesData.out(), unclesCount).swapOut(m_currentUncles);
// Apply rewards last of all.
applyRewards(uncleBlockHeaders);
// Commit any and all changes to the trie that are in the cache, then update the state root accordingly.
m_state.commit();
clog(StateDetail) << "Post-reward stateRoot:" << m_state.rootHash();
clog(StateDetail) << m_state;
clog(StateDetail) << *this;
m_currentBlock.setLogBloom(logBloom());
m_currentBlock.setGasUsed(gasUsed());
m_currentBlock.setRoots(hash256(transactionsMap), hash256(receiptsMap), sha3(m_currentUncles), m_state.rootHash());
m_currentBlock.setParentHash(m_previousBlock.hash());
m_currentBlock.setExtraData(_extraData);
if (m_currentBlock.extraData().size() > 32)
{
auto ed = m_currentBlock.extraData();
ed.resize(32);
m_currentBlock.setExtraData(ed);
}
m_committedToMine = true;
}
void Block::uncommitToMine()
{
if (m_committedToMine)
{
m_state = m_precommit;
m_committedToMine = false;
}
}
bool Block::sealBlock(bytesConstRef _header)
{
if (!m_committedToMine)
return false;
if (BlockInfo(_header, CheckNothing, h256{}, HeaderData).hashWithout() != m_currentBlock.hashWithout())
return false;
clog(StateDetail) << "Sealing block!";
// Compile block:
RLPStream ret;
ret.appendList(3);
ret.appendRaw(_header);
ret.appendRaw(m_currentTxs);
ret.appendRaw(m_currentUncles);
ret.swapOut(m_currentBytes);
m_currentBlock = BlockInfo(_header, CheckNothing, h256(), HeaderData);
cnote << "Mined " << m_currentBlock.hash() << "(parent: " << m_currentBlock.parentHash() << ")";
// TODO: move into Sealer
StructuredLogger::minedNewBlock(
m_currentBlock.hash().abridged(),
"", // Can't give the nonce here.
"", //TODO: chain head hash here ??
m_currentBlock.parentHash().abridged()
);
// Quickly reset the transactions.
// TODO: Leave this in a better state than this limbo, or at least record that it's in limbo.
m_transactions.clear();
m_receipts.clear();
m_transactionSet.clear();
m_precommit = m_state;
return true;
}
State Block::fromPending(unsigned _i) const
{
State ret = m_state;
_i = min<unsigned>(_i, m_transactions.size());
if (!_i)
ret.setRoot(m_previousBlock.stateRoot());
else
ret.setRoot(m_receipts[_i - 1].stateRoot());
return ret;
}
LogBloom Block::logBloom() const
{
LogBloom ret;
for (TransactionReceipt const& i: m_receipts)
ret |= i.bloom();
return ret;
}
void Block::cleanup(bool _fullCommit)
{
if (_fullCommit)
{
// Commit the new trie to disk.
if (isChannelVisible<StateTrace>()) // Avoid calling toHex if not needed
clog(StateTrace) << "Committing to disk: stateRoot" << m_currentBlock.stateRoot() << "=" << rootHash() << "=" << toHex(asBytes(db().lookup(rootHash())));
try
{
EnforceRefs er(db(), true);
rootHash();
}
catch (BadRoot const&)
{
clog(StateChat) << "Trie corrupt! :-(";
throw;
}
m_state.db().commit(); // TODO: State API for this?
if (isChannelVisible<StateTrace>()) // Avoid calling toHex if not needed
clog(StateTrace) << "Committed: stateRoot" << m_currentBlock.stateRoot() << "=" << rootHash() << "=" << toHex(asBytes(db().lookup(rootHash())));
m_previousBlock = m_currentBlock;
m_currentBlock.populateFromParent(m_previousBlock);
clog(StateTrace) << "finalising enactment. current -> previous, hash is" << m_previousBlock.hash();
}
else
m_state.db().rollback(); // TODO: State API for this?
resetCurrent();
}
string Block::vmTrace(bytesConstRef _block, BlockChain const& _bc, ImportRequirements::value _ir)
{
RLP rlp(_block);
cleanup(false);
BlockInfo bi(_block, (_ir & ImportRequirements::ValidSeal) ? CheckEverything : IgnoreSeal);
m_currentBlock = bi;
m_currentBlock.verifyInternals(_block);
m_currentBlock.noteDirty();
LastHashes lh = _bc.lastHashes((unsigned)m_previousBlock.number());
string ret;
unsigned i = 0;
for (auto const& tr: rlp[1])
{
StandardTrace st;
st.setShowMnemonics();
execute(lh, Transaction(tr.data(), CheckTransaction::Everything), Permanence::Committed, st.onOp());
ret += (ret.empty() ? "[" : ",") + st.json();
++i;
}
return ret.empty() ? "[]" : (ret + "]");
}
std::ostream& dev::eth::operator<<(std::ostream& _out, Block const& _s)
{
(void)_s;
return _out;
}