以太坊C++源码解析(四)RLPStream类

RLP是一种特殊的二进制编码解码方式,以太坊里数据包都是采用这种方式编码的,和传统的结构相比,RLP编码更节省空间,提高网络传输效率,缺点就是不太直观,这种编码解码原理介绍在下面这边文章里讲得很好,还附有python的实现代码:RLP编码和解码。但是在本文里我们来看看在C++里的实现。
RLPStream类和RLP类在libdevcore\RLP.hRLP.cpp文件中,前者负责编码,后者负责解码。

RLPStream类

我们先来看看编码方式吧,RLP里面可以放两种数据,一种是字符串(Data),另一种是列表(List),字符串又分为单个字符和多个字符,我们一个一个来。

  • 单个字符
    RLP类提供了这些方法来编码单个字符:
RLPStream& append(unsigned _s) { return append(bigint(_s)); }
RLPStream& append(u160 _s) { return append(bigint(_s)); }
RLPStream& append(u256 _s) { return append(bigint(_s)); }
RLPStream& append(bigint _s);

可见单个字符先都会被转换为bigint,再进行转换。

RLPStream& RLPStream::append(bigint _i)
{
    if (!_i)
        m_out.push_back(c_rlpDataImmLenStart);
    else if (_i < c_rlpDataImmLenStart)
        m_out.push_back((byte)_i);
    else
    {
        unsigned br = bytesRequired(_i);
        if (br < c_rlpDataImmLenCount)
            m_out.push_back((byte)(br + c_rlpDataImmLenStart));
        else
        {
            auto brbr = bytesRequired(br);
            if (c_rlpDataIndLenZero + brbr > 0xff)
                BOOST_THROW_EXCEPTION(RLPException() << errinfo_comment("Number too large for RLP"));
            m_out.push_back((byte)(c_rlpDataIndLenZero + brbr));
            pushInt(br, brbr);
        }
        pushInt(_i, br);
    }
    noteAppended();
    return *this;
}
  1. 如果_i是0,则直接存入c_rlpDataImmLenStart,也就是0x80
  2. 如果_i小于0x80,则直接存入_i
  3. 否则计算存入_i所需要的字节数br,如果br小于c_rlpDataImmLenCount也就是8,则先存入br + 0x80,再调用pushInt(_i, br);存入_i
  4. 计算存入br所需的长度brbr,也就是_i长度的长度,将c_rlpDataIndLenZero + brbr存进去,即0xb7 + brbr,再调用pushInt(_i, br);存入_i
  5. 注意到后面还有个函数noteAppended();这个是用来处理列表长度的,后面再谈。
  • 多个字符
    保存多个字符有下列方法:
RLPStream& append(bytesConstRef _s, bool _compact = false);
RLPStream& append(bytes const& _s) { return append(bytesConstRef(&_s)); }
RLPStream& append(std::string const& _s) { return append(bytesConstRef(_s)); }
RLPStream& append(char const* _s) { return append(std::string(_s)); }
template <unsigned N> RLPStream& append(FixedHash<N> _s, bool _compact = false, bool _allOrNothing = false) { return _allOrNothing && !_s ? append(bytesConstRef()) : append(_s.ref(), _compact); }

方法都是先转换为字节数组bytesConstRef,然后再编码。

RLPStream& RLPStream::append(bytesConstRef _s, bool _compact)
{
    size_t s = _s.size();
    byte const* d = _s.data();
    if (_compact)
        for (size_t i = 0; i < _s.size() && !*d; ++i, --s, ++d) {}

    if (s == 1 && *d < c_rlpDataImmLenStart)
        m_out.push_back(*d);
    else
    {
        if (s < c_rlpDataImmLenCount)
            m_out.push_back((byte)(s + c_rlpDataImmLenStart));
        else
            pushCount(s, c_rlpDataIndLenZero);
        appendRaw(bytesConstRef(d, s), 0);
    }
    noteAppended();
    return *this;
}
  1. 如果是压缩模式,则忽略数组_s开头的0
  2. 如果数组只有单个字符,并且该字符小于0x80,则直接存入该字符
  3. 否则如果数组长度小于0x38,也就是56,则先存入s + 0x80,再调用appendRaw(bytesConstRef(d, s), 0);存入数组本身
  4. 如果数组长度大于等于56,则先存入数组长度s的长度+0xb7,再存入数组长度s,再调用appendRaw(bytesConstRef(d, s), 0);存入数组本身
  5. 也调用了noteAppended();
  • 列表
    存入列表有这些方法:
template <class _T> RLPStream& append(std::vector<_T> const& _s) { return appendVector(_s); }
template <class _T> RLPStream& appendVector(std::vector<_T> const& _s) { appendList(_s.size()); for (auto const& i: _s) append(i); return *this; }
template <class _T, size_t S> RLPStream& append(std::array<_T, S> const& _s) { appendList(_s.size()); for (auto const& i: _s) append(i); return *this; }
template <class _T> RLPStream& append(std::set<_T> const& _s) { appendList(_s.size()); for (auto const& i: _s) append(i); return *this; }
template <class _T> RLPStream& append(std::unordered_set<_T> const& _s) { appendList(_s.size()); for (auto const& i: _s) append(i); return *this; }
template <class T, class U> RLPStream& append(std::pair<T, U> const& _s) { appendList(2); append(_s.first); append(_s.second); return *this; }

我们选这个来看:

template <class _T> RLPStream& appendVector(std::vector<_T> const& _s) { appendList(_s.size()); for (auto const& i: _s) append(i); return *this; }

可见先是调用appendList(_s.size())存入列表大小,然后依次存入列表元素。
但是当我们深入appendList()函数里时,发现不是这么回事:

RLPStream& RLPStream::appendList(size_t _items)
{
    if (_items)
        m_listStack.push_back(std::make_pair(_items, m_out.size()));
    else
        appendList(bytes());
    return *this;
}

这里只是将列表元素个数和当前输出缓冲区大小放入了一个列表栈m_listStack里,并没有存入任何东西。那列表长度是什么时候放进m_out的呢?答案就是前面提到的noteAppended()函数。
noteAppended()函数代码为:

void RLPStream::noteAppended(size_t _itemCount)
{
    if (!_itemCount)
        return;

    while (m_listStack.size())
    {
        /// ...
        m_listStack.back().first -= _itemCount;
        if (m_listStack.back().first)
            break;
        else
        {
            auto p = m_listStack.back().second;
            m_listStack.pop_back();
            size_t s = m_out.size() - p;        // list size
            auto brs = bytesRequired(s);
            unsigned encodeSize = s < c_rlpListImmLenCount ? 1 : (1 + brs);
            auto os = m_out.size();
            m_out.resize(os + encodeSize);
            memmove(m_out.data() + p + encodeSize, m_out.data() + p, os - p);
            if (s < c_rlpListImmLenCount)
                m_out[p] = (byte)(c_rlpListStart + s);
            else if (c_rlpListIndLenZero + brs <= 0xff)
            {
                m_out[p] = (byte)(c_rlpListIndLenZero + brs);
                byte* b = &(m_out[p + brs]);
                for (; s; s >>= 8)
                    *(b--) = (byte)s;
            }
        }
        _itemCount = 1; // for all following iterations, we've effectively appended a single item only since we completed a list.
    }
}

m_out中每存入一个字符或者一个字符串都要调用一次这个函数,这个函数是从后向前遍历m_listStack

  1. 判断当前列表是否全部存完,如果没有存完则退出:
if (m_listStack.back().first)
    break;
  1. 否则先计算列表成员在m_out中实际占用空间的大小:
size_t s = m_out.size() - p;
  1. 再计算出要存入这个s所需空间的大小encodeSize
  2. 将数组整体向后移动encodeSize个空间
memmove(m_out.data() + p + encodeSize, m_out.data() + p, os - p);
  1. 将's'编码并填进空位中。

RLPStream类将数据编码进m_out以后,就可以通过swapOut()函数导出为字节数组了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • 这是以太坊源码研究的第一篇文章。基本上来说,我写什么内容,说明我正好在学习什么内容,并没有固定的顺序。之所以先写R...
    魏兆华阅读 1,497评论 0 0
  • 文章分为2部分, 第一部分是综合整理已有资料而生成的参考文档, 第二部分是python版以太坊代码中的源码实现分析...
    shi_qinfeng阅读 3,556评论 0 3
  • 2018-5-9 晴 这个世界有很多有意思的事情,不是吗? 我们的未来,在希望的田野上,哈哈,希望的田野上。 ...
    蟋蟀王阅读 127评论 0 0
  • 20171228晚 梦境 整体环境偏暗,没有色彩,到处透着诡异的感觉 第一幕 像是一场聚会,有大人,小孩,很多...
    melodyPu阅读 139评论 0 0
  • 不管是一见钟情,还是日久生情,告诉我们“情”的产生场景,都在暗示“爱情”是一个结果,或者说是生命中的一个制高点。而...
    奔跑着的小仓鼠阅读 407评论 0 0