主页 > imtoken官方app > 关注一笔交易,看以太坊内部执行流程

关注一笔交易,看以太坊内部执行流程

imtoken官方app 2023-12-04 05:09:45

作为一个码农,遇到​​现象总想找出其内在原理。 趁着假期,一步步看源码,这个假期会更加充实。

本文初步分析了以太坊中一笔交易的处理流程,涉及交易接收、检查、执行、同步、构建区块和挖矿。 结合之前基于黄皮书的理解和总结,我们对以太坊有了更好的了解。 很多意识。 因为主要工作在c++层面,所以这里使用以太坊c++版本的源码作为学习资源。

如有误会,希望看到的朋友多多指教,共同学习,谢谢。

1.发送交易eth_sendTransaction

// 一个交易框架的构成,实际就是一个交易构成元素
struct TransactionSkeleton
{
	bool creation = false;
	Address from;
	Address to;
	u256 value;
	bytes data;
	u256 nonce = Invalid256;
	u256 gas = Invalid256;
	u256 gasPrice = Invalid256;
	std::string userReadable(bool _toProxy, std::function(TransactionSkeleton const&)> const& _getNatSpec, std::function const& _formatAddress) const;
};
string Eth::eth_sendTransaction(Json::Value const& _json)
{
	try
	{
        // 从Json结构中构建交易骨架
		TransactionSkeleton t = toTransactionSkeleton(_json);
		setTransactionDefaults(t);
		......
            // 向客户端提交这个交易
			h256 txHash = client()->submitTransaction(t, ar.second);
            // 返回交易hash
			return toJS(txHash);
        ......
	
	}
	.... 
}

学习中省略了很多代码,有的是辅助性的,有的可能是功能性的。 这里的目的是梳理一个流程,建立一个框架印象。 省略的代码可以继续学习

这里很简单,从json构造一个事务框架,然后传递给客户端

2.接收交易importTransaction

h256 Client::submitTransaction(TransactionSkeleton const& _t, Secret const& _secret)
{
    // 对交易框架 缺省字段进行补充,最后行程一个交易
    TransactionSkeleton ts = populateTransactionWithDefaults(_t);
    ts.from = toAddress(_secret);
    Transaction t(ts, _secret);
    // 把交易添加到交易队列中
    return importTransaction(t);
}
// 添加交易到交易队列中
h256 Client::importTransaction(Transaction const& _t)
{
    // Client继承于Worker,准备线程处理交易列表
    prepareForTransaction();
    // Use the Executive to perform basic validation of the transaction
    // (e.g. transaction signature, account balance) using the state of
    // the latest block in the client's blockchain. This can throw but
    // we'll catch the exception at the RPC level.
    // 这里借用了Executive的initialize来做基本的交易合法性验证
    // 实际上Executive就是一个交易真正执行的主体,Evm computation,后面还会看到
    Block currentBlock = block(bc().currentHash());
    Executive e(currentBlock, bc());
    e.initialize(_t);
    // 将交易加入到交易队列中
    ImportResult res = m_tq.import(_t.rlp());
    ......
    // 最后返回交易的hash值
    return _t.sha3();
}

客户端收到交易后,借用Executive初步检查交易的合法性,然后将其添加到自己的交易池(m_tq)中,然后继续跟踪m_tq是如何处理的?

3.处理交易

前面提到Client是Worker的子类,它启动线程处理事务

Client::prepareForTransaction -->void startWorking() { Worker::startWorking(); }; --> 工人::工作循环()

void Worker::workLoop()
{
	while (m_state == WorkerState::Started)
	{
		if (m_idleWaitMs)
			this_thread::sleep_for(chrono::milliseconds(m_idleWaitMs));
        // 等待一段定时时间后 缺省30ms,执行工作
		doWork();
	}
}
void Client::doWork(bool _doWait)
{
    ......
        // 同步交易队列,实际内部处理内容比较多
        syncTransactionQueue();
    tick();
    // 开始挖矿 POW
    rejigSealing();
    .....
}

从事务处理到块处理有两个步骤。 一种是同步交易,实际执行交易,然后进行挖矿和计算POW。 下面我们分别来看

3.1 交易同步与执行

void Client::syncTransactionQueue()
{
    ......
        // 这里构建了区块 m_working, 在Block.sync中会执行交易,执行完后,这个m_working就是我们后面要验证的区块了
        // 这里就涉及上上面提到过的 Executive 对象了,它是交易的真正执行者,内容也比较复杂,后面看合适位置再详细记录下
        // 这里的主要过程罗列一下:
        // Block::sync --> ExecutionResult Block::execute(...) --> m_state.execute --> State::executeTransaction ---> _e.initialize  _e.execute  _e.finalize
        //                                                     --> m_transactions.push_back(_t); // 交易执行完 加入到交易列表中
        //                                                     --> m_receipts.push_back(resultReceipt.second);
        tie(newPendingReceipts, m_syncTransactionQueue) = m_working.sync(bc(), m_tq, *m_gp);
    }
   ......
            m_postSeal = m_working;
    DEV_READ_GUARDED(x_postSeal)
        // 更新 bloomer filter
        for (size_t i = 0; i < newPendingReceipts.size(); i++)
            appendFromNewPending(newPendingReceipts[i], changeds, m_postSeal.pending()[i].sha3());
    // Tell farm about new transaction (i.e. restart mining).
    onPostStateChanged();
    // Tell watches about the new transactions.
    noteChanged(changeds);
    // Tell network about the new transactions.
    // 在网络上同步交易,这里跟下去,就会到 P2P网络部分 session host 这些都可以看到了
    // 这里置位host的标记位 m_newTransactions
    // 到了host内部会有如下流程:
    // Host::startedWorking --> h.second->onStarting()(EthereumCapability::onStarting()) --> EthereumCapability::doBackgroundWork()  这里就看到了客户端下面这个函数职位的标记位
    // 然后 EthereumCapability::maintainTransactions() --> m_host->sealAndSend --> Session::sealAndSend --> Session::send --> Session::write()
    if (auto h = m_host.lock())
        h->noteNewTransactions();
   ......
}

上面的代码有很多注释,每一步都可以看到很多内容。

3.2 区块 POW

这里开始涉及到以太坊的POW流程,再补充一些其他的流程代码

3.2.1

POW参与者分为Farm和Miner,即农场和矿工,对应不同的接口

// A miner - a member and adoptee of the Farm.
template  class GenericMiner
// Class for hosting one or more Miners.
template  class GenericFarmFace
   
    //只有一个工作,就是提交工作量证明
    // _p 即找到的解决方案 
    struct Solution
    {
        Nonce nonce;
        h256 mixHash;
    };
    // _finder即方案的发现者
    virtual bool submitProof(Solution const& _p, Miner* _finder) = 0;
// A collective of Miners. Miners ask for work, then submit proofs
template  class GenericFarm: public GenericFarmFace

3.2.2

Ethash,POW的执行算法,在aleth\main.cpp中初始化Ethash

int main(int argc, char** argv)
{
    ......
    Ethash::init();
    ......
}
void Ethash::init()
{
    ETH_REGISTER_SEAL_ENGINE(Ethash);
}
#define ETH_REGISTER_SEAL_ENGINE(Name) static SealEngineFactory __eth_registerSealEngineFactory ## Name = SealEngineRegistrar::registerSealEngine(#Name)
template  static SealEngineFactory registerSealEngine(std::string const& _name) { return (get()->m_sealEngines[_name] = [](){return new SealEngine;}); }
private:
    //单例模式
	static SealEngineRegistrar* get() { if (!s_this) s_this = new SealEngineRegistrar; return s_this; }
最终是new了 Ethash 对象,即一个 SealEngine,最终放入到 SealEngineRegistrar 的 std::unordered_map m_sealEngines;

说完上面步骤的一步步执行,Ethash实例最终保存在SealEngineRegistrar的m_sealEngines中,名字为Ethash

这里也看看Ethash的构建过程

Ethash::Ethash()
{
    // 这里创建一个叫做cpu的矿工,又叫做sealer
    map::SealerDescriptor> sealers;
    sealers["cpu"] = GenericFarm::SealerDescriptor{&EthashCPUMiner::instances, [](GenericMiner::ConstructionInfo ci){ return new EthashCPUMiner(ci); }};
    // 把矿工加入到农场中
    m_farm.setSealers(sealers);
    // 为农场设置onSolutionFound 方法,传入Solution,进行检验
    m_farm.onSolutionFound([=](EthashProofOfWork::Solution const& sol)
    {
        std::unique_lock l(m_submitLock);
//        cdebug << m_farm.work().seedHash << m_farm.work().headerHash << sol.nonce << EthashAux::eval(m_farm.work().seedHash, m_farm.work().headerHash, sol.nonce).value;
        setMixHash(m_sealing, sol.mixHash);
        setNonce(m_sealing, sol.nonce);
        // 对POW的检验
        if (!quickVerifySeal(m_sealing))
            return false;
        if (m_onSealGenerated)
        {
            RLPStream ret;
            m_sealing.streamRLP(ret);
            l.unlock();
            m_onSealGenerated(ret.out());
        }
        return true;
    });
}

综上所述,Ethash中有一个farm(GenericFarm),farm中有一个inspector,名字叫cpu,是EthashCPUMine的一个实例。 EthashCPUMiner 继承GenericMiner 农场继承GenericFarmFace

如何通过名称访问 Ethash 实例

static SealEngineFace* create(std::string const& _name) { if (!get()->m_sealEngines.count(_name)) return nullptr; return get()->m_sealEngines[_name](); }

区块链是如何使用的?

1. 在struct ChainOperationParams 有三类,默认是 NoProof
	/// The chain sealer name: e.g. Ethash, NoProof, BasicAuthority (POA)
	std::string sealEngineName = "NoProof";
2. 在加载配置时,
ChainParams ChainParams::loadConfig 会确定用哪一种,例如名称为 Ethash (pow)
3. 区块链初始化时,会初始化自己的 m_sealEngine
void BlockChain::init(ChainParams const& _p)
     m_sealEngine.reset(m_params.createSealEngine());
SealEngineFace* ChainParams::createSealEngine()
{
    SealEngineFace* ret = SealEngineRegistrar::create(sealEngineName);
	....
}
4. 至此,BlockChain 有了自己的检验引擎,即 m_sealEngine,又即 Ethash 实例

客户端如何获得区块链的Ethash?

SealEngineFace* sealEngine() const override { return bc().sealEngine(); } 这样就获得了上面的检验引擎

3.2.3

上面分析了POW验证者的建立过程,然后按照上面提到的最后一步开始挖矿

void Client::rejigSealing()
{
    if ((wouldSeal() || remoteActive()) && !isMajorSyncing())
    {
        // 这里实际就获得了Ethash的实例
        if (sealEngine()->shouldSeal(this))
        {
           ......
                //这里的m_working就是上面执行完交易后的区块,下面的函数是封装区块内容
                // 由注释就可以知道,最后区块剩下没有完成的部分就是计算nonce mixhash了
                //Sealing
				/// Prepares the current state for mining.
				/// Commits all transactions into the trie, compiles uncles and transactions list, applies all
				/// rewards and populates the current block header with the appropriate hashes.
				/// The only thing left to do after this is to actually mine().
				///
                m_working.commitToSeal(bc(), m_extraData);
            
           ......
            if (wouldSeal())
            {
                sealEngine()->onSealGenerated([=](bytes const& _header) {
                  
                    if (this->submitSealed(_header))
                        m_onBlockSealed(_header);
                    else
                        LOG(m_logger) << "Submitting block failed...";
                });
                
                // 开始挖矿
                sealEngine()->generateSeal(m_sealingInfo);
            }
        }
        ......
    }
    ......
}
void Ethash::generateSeal(BlockHeader const& _bi)
{
    Guard l(m_submitLock);
    m_sealing = _bi;
    m_farm.setWork(m_sealing);
    m_farm.start(m_sealer);
    m_farm.setWork(m_sealing);
}

上面的过程有一些注解,出块,留下nonce mixhasheth以太坊交易,最后调用Ethash开始挖矿计算,最终到达

void EthashCPUMiner::kickOff()

void EthashCPUMiner::startWorking()
{
    if (!m_thread)
    {
         m_shouldStop = false;
         m_thread.reset(new thread(&EthashCPUMiner::minerBody, this));
    }
}
void EthashCPUMiner::minerBody()
{
    setThreadName("miner" + toString(index()));
    auto tid = std::this_thread::get_id();
    static std::mt19937_64 s_eng((utcTime() + std::hash()(tid)));
    // 先计算一个nonce的随机起始值
    uint64_t tryNonce = s_eng();
    // FIXME: Use epoch number, not seed hash in the work package.
    WorkPackage w = work();
    // 根据seedHash找到现在的纪元 DAG
    int epoch = ethash::find_epoch_number(toEthash(w.seedHash));
    auto& ethashContext = ethash::get_global_epoch_context_full(epoch);
    // 获取现在的难度值 
    h256 boundary = w.boundary;
    // 开始穷举nonce,再DAG范围内寻找答案了,即POW的计算过程
    for (unsigned hashCount = 1; !m_shouldStop; tryNonce++, hashCount++)
    {
        auto result = ethash::hash(ethashContext, toEthash(w.headerHash()), tryNonce);
        h256 value = h256(result.final_hash.bytes, h256::ConstructFromPointer);
        if (value <= boundary && submitProof(EthashProofOfWork::Solution{(h64)(u64)tryNonce,
                                     h256(result.mix_hash.bytes, h256::ConstructFromPointer)}))
            break;
        if (!(hashCount % 100))
            accumulateHashes(100);
    }
}

以太坊的核心本质,POW的计算过程就来了。 根据协议的实现eth以太坊交易,会有一个难度值的计算。 我之前总结过。 这里是exhaustive nonce,找到这个值,使其小于边界,找到后提交工作量证明submitProof

onSolutionFound --->Ethash::quickVerifySeal

临时代码整理到这里,基本上就是一个交易过程。 后面我们会详细了解Executive和信息的同步过程等等。

12.31,2018年的最后一天,祝福区块链走向未来,祝大家越来越好