【译】 Sparky: A Lightning Network in Two Pages of Solidity
這個基本想法被稱為支付渠道。 比方說,愛麗絲希望向鮑勃進行大量付款,而無需為每筆交易支付燃氣費。 她建立了一個合同并存放一些乙醚。 對于每筆付款,她向鮑勃發送一封簽署的消息,并說:“我同意給予鮑勃$ 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/
總結
以上是生活随笔為你收集整理的【译】 Sparky: A Lightning Network in Two Pages of Solidity的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【译】IPFS — The Perman
- 下一篇: 【译】Getting Up to Spe