<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by nrek on Medium]]></title>
        <description><![CDATA[Stories by nrek on Medium]]></description>
        <link>https://medium.com/@nrek?source=rss-130a967c9719------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*XkejxsH4UlXJ4y8pSj3Rcw.png</url>
            <title>Stories by nrek on Medium</title>
            <link>https://medium.com/@nrek?source=rss-130a967c9719------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 01:39:43 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@nrek/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[当我们在谈论ERC20时，我们在说什么]]></title>
            <link>https://medium.com/@nrek/%E5%BD%93%E6%88%91%E4%BB%AC%E5%9C%A8%E8%B0%88%E8%AE%BAerc20%E6%97%B6-%E6%88%91%E4%BB%AC%E5%9C%A8%E8%AF%B4%E4%BB%80%E4%B9%88-eb97ebe71f9f?source=rss-130a967c9719------2</link>
            <guid isPermaLink="false">https://medium.com/p/eb97ebe71f9f</guid>
            <category><![CDATA[区块链]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[ethereum]]></category>
            <category><![CDATA[erc20]]></category>
            <dc:creator><![CDATA[nrek]]></dc:creator>
            <pubDate>Sat, 11 Nov 2017 10:52:45 GMT</pubDate>
            <atom:updated>2018-07-10T01:09:28.276Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*4d5O5HPKzETWbS4cVnMmEA.png" /></figure><p>熟悉以太坊代币经济和ICO的同学一定对ERC20这个词不陌生，市面上几乎绝大部分基于以太坊智能合约的项目都宣称自己的代币是ERC20代币，那么究竟什么是ERC20，为什么1个以太坊地址能够作为所有ERC20代币的钱包地址呢？</p><p>ERC20 (Ethereum Request for Comment 20) 是社区在2015年底提出的一项以太坊改进计划 (EIP)，旨在给智能合约的实现提供一个标准，让智能合约能够像那些有自己区块链的原生数字货币 (比如比特币、以太币) 一样具有发送、转账、查询余额等等类似功能。凡是实现了这套标准的智能合约都可称为ERC20代币。发行基于ERC20标准的代币变得很简单，基本不超过10分钟，50行代码。ERC20让代币间变得互相兼容，也增强了代币的交易量和流动性。正是得益于ERC20标准的出现，基于以太坊的去中心化应用开始百花齐放。</p><p>言归正传，ERC20标准其实很简单，一共定义了以下5个函数接口和2个状态接口(event)：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*SOinxxCluyhD6PEg." /></figure><p>下面将结合etherscan.io和EtherDelta上的具体例子 (EETH token, bitcointalk.org上一个恶作剧空投币) 来更直观的解释:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*syTNQQ11thljXdvQ." /></figure><p>• totalSupply和balanceOf(address)</p><p>首先这两个接口很简单，分别是该币的发行总量和给定地址的余额，注意上图中decimals (8) 代表它支持的精度到小数点后8位，所以该地址实际余额是 317047792083 / 10⁸ = 3170.4; 同时注意上图中allowance一项的查询结果为0，将会在下文进行比较和说明。</p><p>• transfer(address _to, uint256 _value)</p><p>这个也比较简单，表示把当前调用该函数用户的_value数量的代币转移给_to这个用户。当然具体实现时需要进行边界条件检查以防止溢出和其它安全问题，现在一般都选择继承自OpenZepplin的SafeMath.sol和StandardToken.sol库。</p><p>• transferFrom, approve, allowance</p><p>这三个接口比较有意思，transferFrom(address _from, address _to, uint256 _value) 接口并不冗余，它是专门给第三方智能合约设计的，表示允许该函数的调用者msg.sender (通常是另一个已授权的智能合约) 从_from账户转移_value个代币到_to账户，同时也会触发Transfer()这个事件在区块链上留下log以便客户端监听。而在调用transferFrom()之前需要让用户先调用 approve(address _spender, uint256 _value) 函数，表示用户授权_spender (即调用transferFrom()的第三方智能合约) 从你的账户最多转移_value个代币。而allowance(address _owner, address_spender) 返回_owner仍然允许_spender转移的代币个数。如果你在EtherDelta进行过交易就会很容易理解这三个函数的意义：在EtherDelta交易的第一步是需要向EtherDelta合约“充值”：</p><p>如果你有所留意，当点击”Deposit”后Metamask会让你确认两次：</p><p>第一次确认的地址0xf152Fc…99c3是EETH contract的地址，这实际上就是调用了approve()函数，允许EtherDalta合约从该账户转走最多3170.478个代币。我们先不点击第二个确认，这时再来观察一下etherscan.io有什么变化：</p><p>可以看到此时余额不变，但是授权EtherDelta挪用的额度变了。这时再点击第二个确认：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/356/0*VLsHYJQSGtTlZcmT." /></figure><p>注意第二次确认的地址0x8d12A1…6819是EtherDelta合约的地址，此时调用了transferFrom()函数，之后再观察下etherscan.io和EtherDelta的变化如下：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*X9vv98Oy56KWbyza." /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/425/0*VZLGQ4fggv2WGCOu." /></figure><p>在etherscan看到此时用户账户余额已经清零了，而且允许EtherDelta再挪用的额度也清零，同时EtherDelta账户显示了应该有的3170.478个EETH。</p><p>• 安全问题</p><p>值得一提的是ERC20的approve()函数存在安全隐患 (front-running attack)，并且该问题至今没有完全解决。可行的攻击场景如下：</p><ol><li>Alice授权Bob可以挪用100个她的Token A. (tx1)</li><li>tx1被矿工确认后，Alice想把授权上限改为50个Token A. (tx2)</li><li>Bob探测到tx1已经确认，同时tx2还在pending状态，他给高额gas并调用transferFrom()函数直接在tx2被确认前从Alice账户转移了100个Token A. (tx3)</li><li>tx3先于tx2被确认，之后不久tx2也被确认，在Alice还没反应过来之前Bob立马再次调用transferFrom()又从Alice那转移了50个Token A。</li><li>这样Bob一共从 Alice那转移了150个Token A，虽然Alice的本意是只希望授权50个给Bob挪用。</li></ol><p>有兴趣的可以参见<a href="https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729">GitHub</a>中具体讨论。虽然社区有人提议如果需要重新设置允许值的话，首先在客户端主动重置允许值为0，等待这笔交易被确认，再检查此期间是否发生过代币转移，最后再设置新的允许值。但是首先这是取决于客户端的实现行为，并不在ERC20标准里；其次哪怕客户端实现了这个方案，上文所述的front-running攻击仍然存在，只是让人们有意识的去多一步检测并发现可能的问题，并没有从根本上解决问题。</p><p>不过也不用太担心，如今实现了ERC20标准的合约千千万，大家都有这问题😂 。而且正如EIP issue里一个评论所提到的，一般用户调用approve(_spender, _value)的场景多是在信任_spender的前提下才会这么调用，而_spender多为交易所的智能合约，一般不会故意想要黑用户的币。然而这个历史遗留问题估计要等到下一版标准出来才有望彻底解决了。</p><p>相关阅读：<a href="http://mp.weixin.qq.com/s?__biz=MzUxMzI1MzM2NA==&amp;mid=2247483674&amp;idx=1&amp;sn=0d9e5c3550c44325a948951c6e8b2511&amp;chksm=f9594b81ce2ec2972640b8262c4a19c0656e0b8e2b9f10eb5bc5a314083921b11fa59f1900ac&amp;scene=21#wechat_redirect">智能合约</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eb97ebe71f9f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[加密货币和区块链的一些资料]]></title>
            <link>https://medium.com/@nrek/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E5%92%8C%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E4%B8%80%E4%BA%9B%E8%B5%84%E6%96%99-421e14dbaec7?source=rss-130a967c9719------2</link>
            <guid isPermaLink="false">https://medium.com/p/421e14dbaec7</guid>
            <dc:creator><![CDATA[nrek]]></dc:creator>
            <pubDate>Fri, 28 Jul 2017 05:53:06 GMT</pubDate>
            <atom:updated>2017-07-28T05:53:06.641Z</atom:updated>
            <content:encoded><![CDATA[<p>加密货币和区块链的一些资料</p><p>大学都与时俱进，纷纷开设了加密数字货币和区块链的课程：</p><p><a href="https://github.com/jonnyhsy/cryptocurrency-and-blockchain-materials">https://github.com/jonnyhsy/cryptocurrency-and-blockchain-materials</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=421e14dbaec7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mac OS X 10.11 EI Capitan下编译Bitcoin源代码]]></title>
            <link>https://medium.com/@nrek/mac-os-x-10-11-ei-capitan%E4%B8%8B%E7%BC%96%E8%AF%91bitcoin%E6%BA%90%E4%BB%A3%E7%A0%81-1a30b8a1bc4a?source=rss-130a967c9719------2</link>
            <guid isPermaLink="false">https://medium.com/p/1a30b8a1bc4a</guid>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[nrek]]></dc:creator>
            <pubDate>Sat, 27 May 2017 18:56:49 GMT</pubDate>
            <atom:updated>2017-05-27T18:56:49.009Z</atom:updated>
            <content:encoded><![CDATA[<pre>git clone <a href="https://github.com/bitcoin/bitcoin">https://github.com/bitcoin/bitcoin</a><br>cd bitcoin<br>git tag<br>git checkout v0.14.1   # choose a recent stable version<br>git branch<br>brew install automake berkeley-db4 libtool boost --c++11 miniupnpc openssl pkg-config protobuf --c++11 qt5 libevent librsvg<br>./autogen.sh<br>./configure<br>make<br>make check   # run tests</pre><p>Note: 如果在编译bitcoind时有链接错误如：</p><pre>CXXLD    bitcoind</pre><pre>Undefined symbols for architecture x86_64:</pre><pre>&quot;_CRYPTO_num_locks&quot;, referenced from:</pre><pre>CInit::CInit() in libbitcoin_util.a(libbitcoin_util_a-util.o)</pre><pre>CInit::~CInit() in libbitcoin_util.a(libbitcoin_util_a-util.o)</pre><pre>&quot;_RAND_cleanup&quot;, referenced from:</pre><pre>CInit::~CInit() in libbitcoin_util.a(libbitcoin_util_a-util.o)</pre><pre>&quot;_CRYPTO_set_locking_callback&quot;, referenced from:</pre><pre>CInit::CInit() in libbitcoin_util.a(libbitcoin_util_a-util.o)</pre><pre>CInit::~CInit() in libbitcoin_util.a(libbitcoin_util_a-util.o)</pre><pre>&quot;_OPENSSL_no_config&quot;, referenced from:</pre><pre>CInit::CInit() in libbitcoin_util.a(libbitcoin_util_a-util.o)</pre><pre>ld: symbol(s) not found for architecture x86_64</pre><pre>clang: error: linker command failed with exit code 1 (use -v to see invocation)</pre><pre>make[2]: *** [bitcoind] Error 1</pre><pre>make[1]: *** [all-recursive] Error 1</pre><pre>make: *** [all-recursive] Error 1</pre><p>则在configure环境时需要明确给出boost库(具体看brew安装的是哪个版本的boost library)的地址：</p><pre>./configure --with-boost-libdir=/usr/local/Cellar/boost/1.64.0_1/lib<br>make clean &amp;&amp; make</pre><ul><li>编译步骤详情可见：<a href="https://github.com/bitcoin/bitcoin/blob/v0.14.1/doc/build-osx.md">https://github.com/bitcoin/bitcoin/blob/v0.14.1/doc/build-osx.md</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a30b8a1bc4a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[重构和调试data pipeline的一点体会]]></title>
            <link>https://medium.com/@nrek/rewriting-and-debugging-a-data-pipeline-job-%E9%87%8D%E6%9E%84%E5%92%8C%E8%B0%83%E8%AF%95data-pipeline%E7%9A%84%E4%B8%80%E7%82%B9%E4%BD%93%E4%BC%9A-49012775996e?source=rss-130a967c9719------2</link>
            <guid isPermaLink="false">https://medium.com/p/49012775996e</guid>
            <dc:creator><![CDATA[nrek]]></dc:creator>
            <pubDate>Sun, 18 Dec 2016 10:04:55 GMT</pubDate>
            <atom:updated>2017-02-28T09:10:05.005Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/910/1*2jJqD5e1n6ezYK6FLeeIbg.png" /><figcaption>Flume execution graph, copied from paper “FlumeJava: Easy, Efficient Data-Parallel Pipelines”</figcaption></figure><p>前一阵花时间用flume on dax 重写了一个很老的MapReduce job, 主要是为了提升性能，万万没想到中间碰到了很多个坑，特意写一篇记录总结下，也方便日后查阅。</p><p>该job本质不复杂，读取~2TB的cookie原始链接(on filesystem)并和Bigtable中的cookie数据(~90TB), join之后计算，输出一个原始的cookie链接图。因为各方面的原因，比如最主要的是读取的那个cookie bigtable有很多其它组也在读它，而且我们需要的column families只replicate在两个数据中心中，这个资源竞争得很激烈；同时我们有8个daily pipeline需要跑这个job (90TB * 8 / day)，这个bigtable服务有时因此还会thorottle掉我们job的读取；还有就是存在某些“热键” － 某些键有很多值聚集在之上，导致shuffle很慢；还有就是我们的job优先级不是特别高，workers常有可能被其它一些production job给preempt(因抢占而被disable)掉；数据中心有时需要做网络、电源等更新而导致网络不稳定，等等等等。所以经常看到这个job跑的慢的时候要15＋个小时，期间收到各种警报邮件，难以容忍。</p><ul><li>flume是Google内部新一代的大数据并行处理框架，不是Apache Flume那个日志处理应用。flume定义了很多操作原语，像PCollection (类似于Spark中的RDD), PTable, ParDo, GroupByKey, CombineValues, Join等等，并且提供了对RecordIO, ColumnIO, BigtableIO, SSTableIO接口的读写处理，很是方便。flume的编程模型本质和Map-Reduce模型不一样，处理流程并不需要强制实现mapper和reducer两个接口，而是简单的类似于”读取数据 -&gt; 并行处理 (compute, filter, map, join, etc.) -&gt; 输出数据”这个流程，整个job的计算流程被分解为一个有向无环图(DAG)，实现起来更接近日常的编程流程 (而不像MapReduce程序，想写个简单的Join操作都麻烦)。目前内部已经有C++，Java，Go三种语言的支持。flume既支持batch data processing，也支持streaming data processing。个人感觉Spark是Flume的一个开源实现，找到一篇FlumeJave的论文 (发表于2010年) 附上：<a href="http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35650.pdf">http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35650.pdf</a></li><li>DAX是<strong>D</strong>istributed <strong>A</strong>pplication E<strong>x</strong>ecution的缩写，它是Google内部的新一代统一的大数据处理平台基石，所有的内部的数据处理管道都可以跑在DAX上，像MapReduce job, Flume job, MillWheel job, Pregel job等等。DAX和flume紧密缝合，使用flume on DAX无需像MapReduce那样手动调整mapper thread, shuffle thread, reducer thread的个数，给定DAX最大并发数它会自己调整(也即Liquid Sharding - 动态分片)。DAX还可以在运行时程序化的限定数据处理的某一部分处理跑在特定的数据中心上，给以特定的资源配额，而对其它处理部分没有影响。DAX将在Google Cloud Platform发挥重要影响，Google Cloud的很多产品，像BigQuery, Cloud DataFlow就是依托于DAX的，往后很多客户托管在Google Cloud的大数据处理方案／程序应该也会借力于DAX。</li></ul><p>＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝</p><p>废话了一大堆，接着写几点我在重写这个job，调试性能，以及在发现计算出的结果和重写前的结果有很大diff的情景下调试的心得。</p><p>1. Production pipeline 和 Canary pipeline:</p><p>这是很重要的一点（另一点是所有提交进代码库的代码都有100%的单元测试覆盖，还有回归测试，系统|端到端测试）。哪怕改动已经测试过了，但是在真正发布前一般还会先发布一个canary (“金丝雀”，报警)版本。Canary版本包含了最近一段时间比如最近一周的改动，Production版本是上一次发布的版本。对于线上服务比如ads server, 会引部分实际流量给canary版本跑一段时间，同时监控重要的指标是否有异常。对于线下服务，比如data pipeline, 也会跑完一整个canary版本看是不是会挂掉，如果数据服务运行正常，那么下面就需要比较上次发布的production版本和这次要发布的canary版本的一些重要指标是否在合理范围内。</p><p>2. Counter diff:</p><p>对于大数据处理程序，计数器(counter)绝对是一个重要指标和调试时重要的参考依据。canary和production之间重要的计数器如果浮动百分比较大，一定要人为确认到底是符合预期的(由于代码改动造成的)，还是因为有bug (可能是因为这次发布包含了bug, 也可能是因为数据源出了问题…) 总之counter diff是一定要做的。如果有必要，还应该把重要的计数器数据导出到dashboard使之可视化，一段时间积累之后可以发现重要的计数器的变化趋势，有时候也能揭示某些隐藏的bug。</p><p>3. Structured data diff tool, A/B test:</p><p>既然是重构/替换一个data pipeline中的某个job，那么比重构后的性能更重要的当然是其正确性，以及如果重构后结果不正确该如何调试。</p><ul><li>当然可以比较该job的计数器 — 但是对于大数据应用程序来说计数器的单位往往是几百万上千万的，那么增减个1000貌似看不出什么来。另外对于我们这个job的计算结果，是每一个cookie和它的连通分量的集合，计数器并不能很好的揭示图的结构，这时我们需要更好的对结构化数据做diff的工具。Google内部广泛采用Protocol Buffer来定义、序列化/反序列化、传输数据，同时使用像SSTable, RecordIO等统一的格式来存储数据，这就让可重用的结构化数据比较工具成为了可能。实际上要diff的数据量也可能很大，比如我们这个job输出的数据就多达2TB，所以内部的可重用的结构化数据比较工具也是用Flume写成的。</li><li>如果重构后计算出的结果不正确，那么只好借助于job的计数器以及结构化数据比较工具计算出的diff来调试，这时控制变量对两个不同版本的job做A/B修改和测试可能可以定位到问题。</li><li>其实还有一个问题，就是怎么肯定计算出的结果就是正确的? 如果production的结果本身就是错的(发布的版本一直有隐藏bug)，那么重写后的job算出来的结果还是错的(bug还在)，错误结果和错误结果比较没什么意义，很可能计数器没什么变化，对计算出的结果做比较也看不出有什么diff。这也表明光是canary V.S. production可能还是不够的，最好能有办法可以采样计算ground truth, 用sampled ground truth来检验/矫正计算出来的结果。(得到ground truth往往是困难的/费时间/费计算资源的，往往能得到采样后的ground truth已经不容易了。比如我们的ground truth其实就存在cookie bigtable里，但是没法一遍一遍的遍历整个bigtable做join) － 这个思路和在机器学习中用训练数据来做训练，但是保留一部分未训练的数据用来做交叉检验是类似的道理。</li></ul><p>4. MapReduce/Flume 程序确定性(deterministic)问题:</p><p>这是在整个调试过程中遇到的最大的坑之一，得到的经验教训就是：保证MapReduce/Join等包含shuffle的操作的步骤中使用的API要是确定性的 —要保证这点可能没有看起来这么简单/明显。API如果仅仅对单个输入是纯函数(pure function)可能还是不够的，API需要对一簇不确定排列顺序的输入是纯函数。对于某个特定的键, shuffle后会有一堆值被聚集起来，但是这些值排列的顺序是不确定的 (当然可以对这些值排序，但是如果API是确定性的，往往可以避免排序这一步；排序不是必要的，而且对热键的值的排序很可能会引起性能问题)，我们需要保证所用的API对排列顺序不确定的值的处理结果是确定的 － 这就是API确定性的含义。调试后问题也是定位在reduce/join时用的一个API上，该API把每个键(cookie id)对应的所有的值(cookie info object) merge起来最终输出1:1的cookie_id : cookie_info_obj，万万没想到对一簇cookie_info_obj merge的API竟然不是纯函数，而且从代码的blame历史来看这个问题已经存在快两年了。。。</p><p>5. 热键检测和过滤, Combiner, Profiling hanging thread:</p><ul><li>不仅仅是I/O、网络传输会导致数据处理程序跑很久，还有一个可能就是数据中存在“热键“，即某个键对应了几万个甚至几十万个值，这种情况也是常见的，而且往往对这种情况没有太多的方法 － 很可能这个键就是业务中需要的，没法提前过滤掉它。Flume提供了很基本的热键检测机制，在flume执行的数据流图界面上会显示每一步前10个处理时间最长的键，以及前10个出现次数最多的键，可以给一些最基本的提示，如果运气好发现的热键是无效的(比如键值为0或负数或其它一些很明显不合理的值)，那可以提前过滤掉这些键。</li><li>处理热键的还有一个办法是：如果reducer的逻辑不依赖于值簇的处理顺序， 比如不需要对值簇排序，则可以考虑使用combiner, 也可以明显加快处理速度。Combiner相当于对每个mapper task的输出做一步pre-reduce，把N个mapper输出变成1个combiner输出，再把每个combiner输出送给reducer再做一步reduer。这种层级reduce可以大大减少热键的reduce端处理消耗的时间。</li><li>存在热键的还有一个明显的现象是某个处理线程/子步骤的处理时间比其它处理线程/子步骤要长很久，这时可以考虑对hanging thread做profiling。这次就遇到了有一个hanging thread要三、四个小时才能做完ParDo/Mapper的处理，连读bigtable都不需要这么久。最初的猜测是CPU大部分都耗费在了某个热键的值簇排序上，但是对这个thread做完profiling才发现猜测是错的，排序大概只花了1%的CPU时间，然而某个merge的API(在ParDo阶段pre-merge的一个API，不是shuffle阶段引起确定性问题的那个API)用了超过85%的CPU时间。最后重构了那个低效的API之后也明显快了许多。(做profiling, 瞎猜测很可能是错的！)</li></ul><p>踩了上面这些坑重写了这个job之后，目前单次最快大概1小时20分跑完，取决于数据中心位置，网络等，平均跑完一次大概在3小时左右，性能明显有了很大的提升。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=49012775996e" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>