【译】 Sparky: A Lightning Network in Two Pages of Solidity

目前,缩放比特币的主要想法是Lightning网络,它可以让人们在脱链时完成大部分交易,并且偶尔会在实际区块链上解决余额问题。 就像我之前描述的拱顶一样 ,在以太坊上它变得更容易。

这个基本想法被称为支付渠道。 比方说,爱丽丝希望向鲍勃进行大量付款,而无需为每笔交易支付燃气费。 她建立了一个合同并存放一些乙醚。 对于每笔付款,她向鲍勃发送一封签署的消息,并说:“我同意给予鲍勃$ X。” 在任何时候,鲍勃都可以将爱丽丝的一条消息发给合同,该合同将检查签名并向鲍勃发送金钱。

诀窍是,鲍勃只能这样做一次。 在他完成之后,合同会记住它已完成,并将剩余的资金退还给Alice。 所以爱丽丝可以给鲍勃发送一系列消息,每一个都有更高的付款。 如果她已经给Bob发送了支付10乙醚的消息,她可以通过发送支付11乙醚的消息向他支付另一个乙醚。

我们还可以添加一个到期日期,之后Alice可以检索她存入的未付款的任何款项。 在此之前,她的资金被锁定。 在截止日期之前,鲍勃非常安全地保持一切线下。 他只需检查余额和截止日期,并确保在截止日期到期之前以最高价格发布消息。

在github上的项目中有示例代码。 该版本使用Ethereum的内置消息系统Whisper。 这基本上工作,但默认情况下不启用,所以它不完全可用。 但任何通信渠道都可以工作。 实际上,这个样本是由EtherAPI制作的,该公司计划使用类似的代码让人们通过HTTP发送微型付款以进行API调用。

实际的智能合约代码在这里 我用魔法摘录了这部分,并简化了它:

 function verify(uint channel, address recipient, uint value, uint8 v, bytes32 r, bytes32 s) constant returns(bool) { PaymentChannel ch = channels[channel]; return ch.valid && ch.validUntil > block.timestamp && ch.owner == ecrecover(sha3(channel, recipient, value), v, r, s); } function claim(uint channel, address recipient, uint value, uint8 v, bytes32 r, bytes32 s) { if (!verify(channel, recipient, value, v, r, s)) return; if (msg.sender != recipient) return; PaymentChannel ch = channels[channel]; channels[channel].valid = false; uint chval = channels[channel].value; uint val = value; if (val > chval) val = chval; channels[channel].value -= val; if (!recipient.call.value(val)()) throw;; } 

这个合同可以处理很多付款渠道,每个都有一个所有者。

Alice向Bob发送一条消息,其中包含以下值:

  • 她使用的频道的ID(因为此合约可处理大量频道)
  • 收件人,即Bob的地址
  • 她付款的价值
  • 她的签名,由三个数字v,r,s(一个标准的椭圆曲线签名)

验证功能通过对频道ID,收件人和值进行散列开始。 sha3函数可以接受任意数量的参数,并将它们混合在一起并散列它们:

 sha3(channel, recipient, value) 

为了验证签名,我们使用ecrecover函数,该函数采用散列和签名(v,r,s),并返回产生该签名的地址。 我们只是检查确认签名是由渠道所有者完成的:

 ch.owner == ecrecover(sha3(channel, recipient, value), v, r, s); 

确保频道仍然有效,并且截止日期没有通过,我们正在验证。 索赔函数首先调用验证,如果返回true,则将钱发送给Bob并将channel.valid设置为false,以便Bob不能再提取任何更多资金。

如果爱丽丝透支她的资金,取决于鲍勃是否停止接受她的付款。 如果他拧了,我们检查一下; 如果资金透支,我们会将付款减少到频道中的可用资金。

像这样的单向渠道非常像盲目拍卖 只有鲍勃被允许打电话给索赔(),他的动机是要求尽可能多的钱,这正是我们想要发生的事情。

双工频道

假设爱丽丝和鲍勃希望彼此频繁发生小额支付。 他们可以使用两个渠道,但这意味着在资金用尽时每个渠道都会关闭,即使他们的净余额变化不大。 如果我们有双向渠道,付款流向两个方向,会更好。

一种方法是让一方提交当前状态(即双方的余额),并允许对方提交更近期的状态。 这适用于任何类型的状态通道,但它有点复杂。 我们必须包含随每条消息递增的随机数; 如果爱丽丝和鲍勃同时向对方发送消息会怎么样?

对于简单的价值转移有一个更简单的方法。 消息发送者到目前为止所发送的资金总额中不会包含净余额,而只是添加消息。 合约数字为渠道关闭时的净余额。 这使我们不必担心消息排序。 我们可以相信双方都可以发送他们最近的收据,因为这将是最大收入的收据。

为了计算给Alice的净支付,我们取Alice的余额,加上Alice的总应收账款,并减去Bob的总应收账款。 如果应收账款超过了余额,那就意味着这笔钱来回走了很多。 和以前一样,如果有人透支,我们会调低应收账款。

为了做到这一点,我们从索赔函数中删除了立即的以太转账,并且在两个索赔提交后让每一方退出。 如果一方在截止日期前未提交索赔,我们认为他们没有收到任何款项。 攻击者可能试图发送垃圾邮件以阻止对方提交收据; 为了减轻这种影响,我们需要确保频道在第一次索赔后的最短时间内保持打开状态。

渠道网络

但闪电不仅仅是两方支付渠道。 如果您不得不在支付渠道中为您想要支付多少次的所有人存入一堆钱,那么现金流量将非常困难。 闪电应该让你通过中间人路由支付。 通过支付渠道网络,只要您可以通过网络找到通往收款人的路径,就可以将付款路由到您想要的任何地方。

如果您不知道比特币操作码,Lightning (pdf)很难详细了解,但我不知道。 但是最近我发现了一篇精彩的小文章 ,描述了这个基本概念,它非常简单和优雅,并且认识到在Ethereum上实现它很容易。

假设爱丽丝想要向卡罗尔支付以太币10。 她没有通往卡罗尔的频道,但她确实有一个频道给Bob,他有一个去Carol的频道。 所以付款需要从Alice到Bob到Carol。

Carol创建了一个随机数,我们将其称为Secret,并将其散列为HashedSecret。 她将HashedSecret交给Alice。

Alice向Bob发送消息,就像两方支付通道消息一样,但添加了HashedSecret。 为了要求这笔钱,鲍勃必须将这个信息与匹配的秘密一起提交给合同。 他必须从卡罗尔那里得到这个秘密。

所以他给卡罗尔发了一个类似的消息,用相同的支付价值减去他的服务费。 服务费用不必在合同中执行; 每个节点只需向下一个节点发送稍小的支付。

卡罗尔当然已经有了秘密,所以她可以立即向鲍勃索取她的资金。 如果她这样做,那么鲍勃会在区块链上看到秘密,并能够从爱丽丝那里索取他的资金。

但不是这样做,她可以将秘密发送给Bob。 现在鲍勃可以从爱丽丝那里取回他的钱,即使卡罗尔从未再次触及区块链。

所以在这一点上:

  • 卡罗尔能够通过提交他的签名声明和匹配秘密从鲍勃那里获得资金。
  • 鲍勃也有这个秘密,所以他能够从爱丽丝那里领取他的钱
  • 鲍勃把这个秘密发给爱丽丝,以便她证实卡罗得到了付款

在我们进行新的付款时,我们与两方渠道一样,只是更新总额。 这意味着收件人只需保留最新的秘密。

为了完成所有这些工作,我们所要做的只是稍微修改我们的验证和声明函数:

 function verify(uint channel, address recipient, uint value, bytes32 secret, uint8 v, bytes32 r, bytes32 s) constant returns(bool) { PaymentChannel ch = channels[channel]; if !(ch.valid && ch.validUntil > block.timestamp) return false; bytes32 hashedSecret = sha3(secret) return ch.owner == ecrecover(sha3(channel, recipient, hashedSecret, value), v, r, s); } function claim(uint channel, address recipient, uint value, bytes32 secret, uint8 v, bytes32 r, bytes32 s) { if( !verify(channel, recipient, value, secret, v, r, s) ) return; 

现在签名在频道,收件人,hashedSecret和值的sha3上。 我们正在传递秘密,并验证它是否与签名中的内容有关。

提前关机

想象一下,爱丽丝想支付戴夫,并通过鲍勃,然后卡罗尔支付的付款。 所以这是ABCD付款。 假设这是BC频道中的第一笔支付,所以Bob向卡罗累计的累计付款余额只是ABCD金额。 但戴夫从未揭示这个秘密。

现在埃迪想要付费给弗雷德,也是通过鲍勃和卡罗尔支付EBCF。

为了处理EBCF,Bob必须在Carol's之上添加Eddie的付款金额,因此BC上的累计付款总额为ABCD + EBCF。 但卡罗尔可以用来自弗雷德的秘密赎回这种平衡。

鲍勃可以使用弗雷德的秘密向埃迪索取这笔钱。 但是,如果没有戴夫的秘密,他不能向爱丽丝索取这笔钱,所以他在ABCD支付金额上有所损失。

因此,鲍勃必须避免在BC频道上投入新的支付,同时还有一个未公开的秘密。 (很有可能认为他可以发布EBCF的总数,假设ABCD不存在,但如果秘密稍后公布,该怎么办?)

这意味着我们应该让节点尽早关闭它们的通道,以便它们可以在停止时重新启动。 随着时间的推移,人们会选择可靠的合作伙伴

这也意味着通道完全同步,这对于可伸缩性解决方案来说并不理想。 快速的网络服务器不会一次处理一个请求; 他们可以接受大量的请求并在准备就绪时发送每个响应。 但是闪电频道必须经过一个完整的请求 - 响应才能接受另一个请求。 比特币的闪电也是如此。 尽管如此,与将每笔交易放在连锁店相比,我们可以做得很好。

也许这些同步渠道有助于避免集中化。 由于每个信道的吞吐量都有限,因此用户最好通过低业务量的信道进行路由。

路由

说到路由,这很简单,因为我们可以在客户端本地执行所有操作。 所有通道都设置在链上,因此客户端可以将它们全部读入内存并使用它喜欢的任何路由算法。 然后它可以在脱链消息中发送完整的路由。 这也可以让发件人计算出所有中间商将收取的总交易费用。

为了简化这一点,我们可以使用事件来记录每个新频道。 JavaScript API可以查询最多三个索引属性,因此我们索引两个端点地址和过期。 我们还会记录每个地址的脱链联系信息; 它可能是http,电子邮件,无论如何。 javascript查询频道,询问终端有多少可用资金,并构建路线。

合同

据我所知,我所描述的几乎是Lightning所做的,我们可以用两页长的合同来实现它。 我们需要客户端代码来处理路由和消息传递,但链上的基础架构非常简单,并且不会对以太坊进行任何更改。 这是一些完全未经测试的整个合同的代码。

 contract Lightning { modifier noeth() { if (msg.value > 0) throw; _ } function() noeth {} uint finalizationDelay = 10000; event LogUser(address indexed user, string contactinfo); event LogChannel(address indexed user, address indexed bob, uint indexed expireblock, uint channelnum); event LogClaim(uint indexed channel, bytes32 secret); struct Endpoint { uint96 balance; uint96 receivable; bool paid; bool closed; } struct Channel { uint expireblock; address alice; address bob; mapping (address => Endpoint) endpoints; } mapping (uint => Channel) channels; uint maxchannel; function registerUser(string contactinfo) noeth { LogUser(msg.sender, contactinfo); } function makeChannel(address alice, address bob, uint expireblock) noeth { maxchannel += 1; channels[maxchannel].alice = alice; channels[maxchannel].bob = bob; channels[maxchannel].expireblock = expireblock; LogChannel(alice, bob, expireblock, maxchannel); } function deposit(uint channel) { Channel ch = channels[channel]; if (ch.alice != msg.sender && ch.bob != msg.sender) throw; ch.endpoints[msg.sender].balance += uint96(msg.value); } function channelExpired(uint channel) private returns (bool) { return channels[channel].expireblock < block.number; } function channelClosed(uint channel) private returns (bool) { Channel ch = channels[channel]; return channelExpired(channel) || (ch.endpoints[ch.alice].closed && ch.endpoints[ch.bob].closed); } //Sig must be valid, //signer must be one endpoint and recipient the other function verify(uint channel, address recipient, uint value, bytes32 secret, uint8 v, bytes32 r, bytes32 s) private returns(bool) { bytes32 hashedSecret = sha3(secret); address signer = ecrecover(sha3(channel, recipient, hashedSecret, value), v, r, s); Channel ch = channels[channel]; return (signer == ch.alice && recipient == ch.bob) || (signer == ch.bob && recipient == ch.alice); } function claim(uint channel, address recipient, uint96 value, bytes32 secret, uint8 v, bytes32 r, bytes32 s) noeth { Channel ch = channels[channel]; Endpoint ep = ch.endpoints[recipient]; if ( !verify(channel, recipient, value, secret, v, r, s) || channelClosed(channel) || ep.receivable + ep.balance < ep.balance ) return; ep.closed = true; ep.receivable = value; //if this is first claim, //make sure other party has sufficient time to submit claim if (!channelClosed(channel) && ch.expireblock < block.number + finalizationDelay) { ch.expireblock = block.number + finalizationDelay; } LogClaim(channel, secret); } function withdraw(uint channel) noeth { Channel ch = channels[channel]; if ( (msg.sender != ch.alice && msg.sender != ch.bob) || ch.endpoints[msg.sender].paid || !channelClosed(channel) ) return; Endpoint alice = ch.endpoints[ch.alice]; Endpoint bob = ch.endpoints[ch.bob]; uint alicereceivable = alice.receivable; uint bobreceivable = bob.receivable; //if anyone overdrew, just take what they have if (alicereceivable > bob.balance + bob.receivable) { alicereceivable = bob.balance + bob.receivable; } if (bobreceivable > alice.balance + alice.receivable) { bobreceivable = alice.balance + alice.receivable; } uint alicenet = alice.balance - bobreceivable + alicereceivable; uint bobnet = bob.balance - alicereceivable + bobreceivable; //make double sure a bug can't drain from other channels... if (alicenet + bobnet > alice.balance + bob.balance) return; uint net; if (msg.sender == ch.alice) { net = alicenet; } else { net = bobnet; } ch.endpoints[msg.sender].paid = true; if (!msg.sender.call.value(net)()) throw; } } 

令牌

上面的代码用ether来完成所有的事情。 但将其扩展到使用其他令牌并不难。 创建频道时设置令牌地址,更改存款和取款功能,即完成。

进一步阅读

雷电网络是以太坊闪电式网络的着名实现。 其Solidity代码显得更加复杂; 与Sparky相比,它使用ERC20令牌代替以太网,具有不同的结算机制,并使用一些汇编进行性能优化。 该项目也包括所有的非连锁基础设施。

这里有一篇关于以太坊和比特币支付渠道网络的文章 ,里面有一些有趣的想法。

Vitalik最近在一次金融会议上描述了国家渠道。


https://www.blunderingcode.com/a-lightning-network-in-two-pages-of-solidity/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值