Ripple共识过程中的各种View
一条交易从发出到落链的过程
- 客户端通过ws,http发出签名/未签名的交易
- 节点接收到交易并检查签名
- 节点程序本地走一遍对交易的验证,如果通过,广播并投赞成票,如果不通过,将错误返给客户端
- 交易被广播后,其它节点同样走验证过程,并对交易投票
- 交易最终超过一定百分比(这个随时间增加会增大)的赞成票,则进入交易候选集,否则,下一次共识继续投票
- 进入交易集的交易会在生成区块时进行应用,应用的过程也是走本地验证,如果能成功apply,则被包含进本地区块,否则,进入到下一个OpenView中去apply
- 本地生成区块后广播区块hash,达到一定百分比的区块hash会共识通过,与这一hash不同的,向其它节点获取区块内容
在上面的过程中,Ripple是通过什么机制保证交易应用过程的原子性(就是交易可能前半部分改变了状态,后半部分失败了,这时候要全部回滚)呢,在以太坊中有快照,交易执行不成功可以回滚,比特币只有一个转账交易,比较简单,本身可以认为是原子操作。
Ripple中的View
Ripple中
基类有:RawView,ReadView,ApplyView,
中间类:TxsRawView,DigestAwareReadView,ApplyViewBase
实现类:OpenView,Ledger,ApplyViewImpl
继承关系:
TxsRawView : Rawview
DigestAwareReadView : ReadView
ApplyViewBase : ApplyView
OpenView : TxsRawView,ReadView
Ledger : TxsRawView,DigestAwareReadView
ApplyViewImpl : ApplyViewBase
RawView的作用:
Interface for ledger entry changes.
Subclasses allow raw modification of ledger entries.
其中的方法都可以无条件修改SLE
ReadView:
A view into a ledger.
This interface provides read access to state
and transaction items. There is no checkpointing
or calculation of metadata.
Ripped中通过ApplyView来完成沙盒的功能
先看一段代码:
RCLConsensus::Adaptor::buildLCL
{
...
// Build the new last closed ledger
auto buildLCL =
std::make_shared<Ledger>(*previousLedger.ledger_, closeTime);
...
{
OpenView accum(&*buildLCL);
assert(!accum.open());
if (replay)
{
// Special case, we are replaying a ledger close
for (auto& tx : replay->txns_)
applyTransaction(
app_, accum, *tx.second, false, tapNO_CHECK_SIGN, j_);
}
else
{
// Normal case, we are not replaying a ledger close
retriableTxs = applyTransactions(
app_, set, accum, [&buildLCL](uint256 const& txID) {
return !buildLCL->txExists(txID);
});
}
// appy to Ledger
accum.apply(*buildLCL);
}
...
}
这是共识过程中,生成要广播的区块的代码,里面的buildLCL就是要生成的区块,可以看到,这中间的过程是这样的:
- 先根据buildLCL生成一个OpenView
- 将交易应用到OpenView对象中,去改变SLE状态,并收集真正能成功应用的交易
- OpenView对象反过来应用到buildLCL
过程3对应的代码:
void
OpenView::apply (TxsRawView& to) const
{
// apply sle change
items_.apply(to);
// insert txs
for (auto const& item : txs_)
to.rawTxInsert (item.first,
item.second.first,
item.second.second);
}
这里的to就是上面的buildLCL,items_.apply(to); 会改变状态树SHAMap,to.rawTxInsert会改变交易树SHAMap,最终两个树根hash会参与到LedgerHash的计算中。
上面的代码可以说是用OpenView来过滤了一次交易,将侯选集中的交易再次应用一遍,然后把结果 应用到Ledger中,那么,如何保证OpenView中操作的原子性呢?
这就要看具体的验证过程了applyTransaction会调到apply.cpp中的apply方法,这是一个全局方法,定义如下:
std::pair<TER, bool>
apply (Application& app, OpenView& view,
STTx const& tx, ApplyFlags flags,
beast::Journal j)
{
STAmountSO saved(view.info().parentCloseTime);
auto pfresult = preflight(app, view.rules(), tx, flags, j);
auto pcresult = preclaim(pfresult, app, view);
return doApply(pcresult, app, view);
}
这跟交易刚发到节点时进行的本地验证其实是一个过程。
在这个过程中preflight与preclaim都是检查工作,最终会改变sle的是doApply操作
std::pair<TER, bool>
doApply(PreclaimResult const& preclaimResult,
Application& app, OpenView& view)
{
if (preclaimResult.view.seq() != view.seq())
{
// Logic error from the caller. Don't have enough
// info to recover.
return{ tefEXCEPTION, false };
}
try
{
if (!preclaimResult.likelyToClaimFee)
return{ preclaimResult.ter, false };
ApplyContext ctx(app, view,
preclaimResult.tx, preclaimResult.ter,
preclaimResult.baseFee, preclaimResult.flags,
preclaimResult.j);
return invoke_apply(ctx);
}
catch (std::exception const& e)
{
JLOG(preclaimResult.j.fatal()) <<
"apply: " << e.what();
return { tefEXCEPTION, false };
}
}
在doApply中用到了ApplyContext,这个类比较关键,可以看到它的构造函数传入了openview对象,构造函数如下 :
ApplyContext::ApplyContext(Application& app_,
OpenView& base, STTx const& tx_, TER preclaimResult_,
std::uint64_t baseFee_, ApplyFlags flags,
beast::Journal journal_)
: app(app_)
, tx(tx_)
, preclaimResult(preclaimResult_)
, baseFee(baseFee_)
, journal(journal_)
, base_ (base)
, flags_(flags)
{
view_.emplace(&base_, flags_);
}
private:
boost::optional<ApplyViewImpl> view_;
ApplyContext中的view是根据openview对象构造出来的ApplyViewImpl类型的对象,在doApply执行过程中,用到的都是这个view_对象,就是这个view_起到了关键作用,因为最终是要改变openView才能生效,改变OpenView是在交易完全共识过才进行的:
static
std::pair<TER, bool>
invoke_apply (ApplyContext& ctx)
{
switch(ctx.tx.getTxnType())
{
case ttACCOUNT_SET: { SetAccount p(ctx); return p(); }
case ttOFFER_CANCEL: { CancelOffer p(ctx); return p(); }
case ttOFFER_CREATE: { CreateOffer p(ctx); return p(); }
case ttPAYMENT: { Payment p(ctx); return p(); }
...
}
//每种交易类都是继承自Transactor类的
std::pair<TER, bool>
Transactor::operator()()
{
...
auto terResult = ctx_.preclaimResult;
if (terResult == tesSUCCESS)
terResult = apply(); //里面调用各交易类的doApply方法
bool didApply = isTesSuccess (terResult);
if (didApply)
{
// Transaction succeeded fully or (retries are
// not allowed and the transaction could claim a fee)
...
//真正改变OpenView的是这行代码
ctx_.apply(terResult);
// since we called apply(), it is not okay to look
// at view() past this point.
}
}
TER Transactor::apply ()
{
preCompute();
...
//调用各个子类的doApply方法
return doApply();
}
//ApplyViewImpl的内容应用到OpenView上
void
ApplyContext::apply(TER ter)
{
view_->apply(base_, tx, ter, journal);
}
也就是说,只有交易类的doApply方法真的完全成功了,结果才会反映到OpenView中,OpenView再将结果转移到Ledger中,Ripple中就是用这种方式对交易进行原子式的应用,不会出现交易前面修改了SLE,后面又失败了,导致SLE的修改不可逆的情况。