链下支付协议(微雷电microRaiden深入分析)

microRaiden是雷电网络的简化版本,是一种低成本、可扩展、低延时的链下微支付解决方案。 他将雷电网络中链下支付网络简化为P2P单向微支付通道, 但保留了快速支付的优势,降低部署难度,简化支付流程。
微雷电基于以太坊开发, 是以太坊的二层支付协议。兼容ERC20/ERC223标准token接口,因此,可以直接将现有已部署在以太坊上的token和微雷电协议对接,而不需要进行token数据的迁移。
1. microRaiden的特点及应用场景 microRaiden是针对N对1 的商店模式而专门优化的一种支付通道,特点如下:

  1. 支持ERC20/ERC223标准的token;
  2. 没有支付网络, 只有直连单向支付通道;
  3. 支付前需要建立支付通道(要花gas);
  4. 只能由支付者向指定的收款者创建支付通道;
  5. 接收者可以随时直接关闭通道; 支付者想关闭通道,需要接收者的收款证明;
针对上述特点,以及微雷电设计初衷,它适用于下面的应用场景:
  • 接收者地址相对固定
  • 发送者建立一条通道后不需要立即拆除通道
  • 在同一通道内有较多的支付需求
场景举例:接收者提供很多网络资源(jpg, url, txt等等),每个资源可以分别设置单价。用户访问该资源时, 自动创建channel,并自动从channel的押金中扣去该资源的价格,之后也可以通过该channel多次访问资源,每次访问自动扣除相应的token。
2. microRaiden项目概述 microRaiden包括客户端和服务端,客户端即sender,每个sender都分别对应一个客户端,服务端即receiver。
  • microRaiden有2种类型的客户端, 带UI的web客户端, 无UI的python客户端,两个客户端都可以发起创建等channel相关的操作;
  • microRaiden项目实现了一个网站付费资源后端服务:在后端运行一个PaywalledProxy代理,用于channel的管理和"资源"管理;
  • 如果想在自己的项目中支持microRaiden支付协议, 可以直接包含使用python版本的lib:Client;
  • PaywalledProxy代理启动的时候,需要注册出售的"资源",包括名称,价格等;
  • 用户通过web网页或python客户端访问该"资源", 就会自动触发创建一个新的channel, 并立即支付资源对应的价格;
3. 微雷电客户端 微雷电客户端包括Session Client Channel等几大数据结构,关系如下:
链下支付协议(微雷电microRaiden深入分析)
文章图片
image 上面的图是官方生成的, 了解一下3个主要结构的主要功能就行:
链下支付协议(微雷电microRaiden深入分析)
文章图片
image
  • Client: 和以太坊智能合约进行交互,包括创建channel等;
  • Channel: 管理本地链下channel的数据;
  • Session:代表客户端和服务端的一个会话,处理http请求和响应,封装了Client和Channel的操作,是客户端最外层的接口。
客户端主要功能如下:
  1. 余额证明的签名及地址恢复;
  2. 链上链下的channel信息同步;
  3. 管理channel:新建,查询,获取指定channel,channel的deposit追加;
  4. 和服务端的http交互,支持自动链下支付;
3.1 balanceProof 余额证明 余额证明的含义是sender对当前channel的最新一笔支付数据进行签名,证明sender已经对购买的资源进行了支付。 这里的余额实际上指的是该channel上已花费的token数量。
  • balanceProof的签名数据格式:
    [ //type,name,data ('string', 'message_id', 'Sender balance proof signature'), ('address', 'receiver', receiver), ('uint32', 'block_created', (open_block_number, 32)), ('uint192', 'balance', (balance, 192)), ('address', 'contract', contract_address) ]

  • 链下生成一个balanceProof算法
    接口: \microraiden[crypto.py](http://crypto.py):sign_balance_proof 函数,算法如下:
    bytes32 balance_message_hash = keccak256( keccak256( 'string message_id', 'address receiver', 'uint32 block_created', 'uint192 balance', 'address contract' ), keccak256( 'Sender balance proof signature', _receiver_address, _open_block_number, _balance, address(this) ) );

  • 链上对balanceProof进行验证(用于web客户端)
    接口: RaidenMicroTransferChannels合约: verifyBalanceProof 函数
    根据receiver,channelID,balance地址,从一个余额证明的签名中恢复并返回sender的地址
  • 链下对balanceProof进行验证(用于python客户端)
    接口: \microraiden[crypto.py](http://crypto.py). verify_balance_proof
    验证结果是返回签名者(即sender)的地址。
3.2 Client.sync_channels 同步链上(合约中)channel信息到链下 在初始化Client时需要同步链上的channel信息到本地。 同步流程如下:
  1. 查询channelManager合约中所有已经执行的交易的事件(包括 ChannelCreated,ChannelToppedUp,ChannelCloseRequested,ChannelSettled事件),过滤条件是本sender地址。
  2. 从交易事件中解析出sender,receiver,blockNumber, 作为key, 查询本地是否已建立channel,依次处理下面的交易事件:
    2.1 对于ChannelCreated事件:如果存在, 更新存款(deposit);如果没有,新建一个Channel结构
    2.2 对于ChannelToppedUp事件:更新deposit到本地
    2.3 对于ChannelCloseRequested事件:更新本地的balance(已花费的数量)和balance签名,并把状态更新为settling
    2.4 对于ChannelSettled事件:把状态更新为closed
3.3 Client.get_suitable_channel 客户端获取'合适'的channel 该接口可以作为获取channel的唯一入口, 其中包含了查询已存在的channel, 新建一个channel的接口调用封装。
  1. 查询和指定receiver的所有已经打开的channel(实际上只有1条), 如果存在并已经打开,并有足够的deposit,返回该channel;如果存在但没有足够的deposit,进行topup后返回该channel
  2. 如果不存在,则新建channel并返回,新建channel见下面小节
3.4 Client.open_channel 新建一条channel 实现链上channel相关的操作。
  1. 转账deposit数量的token给channel管理合约地址, transfer的data指定receiver地址(离线签名方式发送)。离线签名的交易通过contract_proxy.token_proxy的 create_signed_transaction 实现,链上的流程间3.4.1小节。
  2. 监听并等待channel的创建事件。通过contract_proxy.channel_manager_proxy的 get_channel_created_event_blocking 实现
  3. 监听到创建channel创建成功的事件后,构造Channel结构,保存该channel到Session。
3.4.1 链上创建channel的流程 一句话概括: token转账动作自动触发创建channel
一个sender如果希望和指定的receiver建立一条支付channel,并在这条channel上存款10token,只需要给ChannelContract合约地址转入存款数目的token即可。注意: 所有地址的余额是保存在token合约中的,这里的地址不限于个人账户地址,也可以是普通合约的地址。
创建支付通道是由微雷电客户端发起的,流程如下:
链下支付协议(微雷电microRaiden深入分析)
文章图片
image
  1. sender通过webApp输入存款数目10,和receiver的地址;
  2. webApp向ERC223 token合约发起一笔交易: 即调用该合约的transfer函数,参数为:
    2.1 目标地址: 即channels管理合约(ChannelContract)地址;
    2.2 目标值:即存款10个token;
    2.3 data:自定义数据,这里是receiver的地址;
  3. ERC223 token合约的transfer函数按照标准定义的实现方式,会调用目标地址的tokenFallback函数。也就是说, 当sender向目标地址进行transfer转账时,如果目标地址是合约地址(在本例子中是ChannelsContract合约),则会调用该合约的定义的tokenFallback函数;
  4. ChannelsContract合约的tokenFallback函数是真正实现创建channel和更新存款的操作:
    4.1 创建channel: createChannelPrivate(sender,receiver,deposit)
    4.1.1 获取当前blockNumber作为open_block_number;
    4.1.2 用这三个参数sender,receiver,open_block_number进行sha3,得到一个hash,作为本channel的key, value是Channel结构 {deposit,open_block_number} ;
    4.1.3 触发ChannelCreated事件通知webApp;
    4.2 更新存款: updateInternalBalanceStructs(sender,receiver,block_number,deposit)
    4.2.1 同创建channel类似,block_number作为open_block_number,更新Channel结构的deposit;
    4.2.2 触发ChannelToppedUp事件通知webApp;
对于ERC20标准的token, 处理流程如下:
需要2笔交易: approve和createChannel
链下支付协议(微雷电microRaiden深入分析)
文章图片
image 3.5 对一条channel进行充值(追加押金) 充值和创建类似, 只是data数据变成了:
_data = https://www.it610.com/article/sender_address[2:] + receiver_address[2:] + hex(open_block_number)[2:].zfill(8) _data = bytes.fromhex(_data)

链下支付协议(微雷电microRaiden深入分析)
文章图片
image 同样, 兼容erc20的流程如下:
链下支付协议(微雷电microRaiden深入分析)
文章图片
image 需要2笔交易:approve和topUp
3.6 channel的关闭 sender和receiver都可以发起channel关闭的请求,但是处理流程稍有不同:
  1. receiver使用sender的余额签名发起关闭channel操作
    处理流程如下:
    a. 检查余额签名者是不是sender;
    b. 对channel进行结算(剩余deposit给sender,支付金额给receiver);
    c. 关闭可能存在的sender发起的对该channel的“挑战”;
  2. sender使用receiver的关闭签名发起关闭channel操作
    处理流程如下:
    a. 检查余额签名者是不是sender;
    b. 检查关闭签名者是不是receiver;
    c. 对channel进行结算;
  3. sender没有使用receiver的关闭签名发起关闭channel操作
    处理流程如下:
    a. 检查余额签名者是不是sender
    b. 发起一个”挑战“,默认挑战周期是30个block时间;此时channel是没有关闭的,只是记录了该channel被sender发起了一个关闭”挑战“。
    有2种情况:
    1. 在挑战期间,receiver使用sender的余额签名发起关闭channel操作;表示channel正常关闭;
    2. 在“挑战”周期结束后(意味着receiver没有在挑战期间关闭该channel,而且对该关闭没有异议),sender调用settle操作;表示sender对该channel进行结算, 并正常关闭channel;
3.7 Session :一个支持微雷电支付协议的requests包 Session是客户端的核心,通过它可以完成购买资源的操作。
  1. 创建一个Session实例
  2. response = session.get('{}/{}'.format(endpoint_url, resource))
python的requests包是一个优秀的http操作包,可以方便地进行http交互操作,比如:
>>> r = requests.put("http://httpbin.org/put") >>> r = requests.delete("http://httpbin.org/delete") >>> r = requests.head("http://httpbin.org/get") >>> r = requests.options("http://httpbin.org/get")

为了更方便的在微雷电中使用,对其进行扩展, 重新封装了 get,options,head,post,put,patch,delete 这些requests接口。
在微雷电的客户端中包含该扩展后的包,可以在http请求资源的同时,进行token链下支付操作。
例如post接口:
def post(url: str, **kwargs) -> Response: return request('POST', url, **kwargs)def request(method: str, url: str, **kwargs) -> Response: session_kwargs = pop_function_kwargs(kwargs, Session.__init__) client_kwargs = pop_function_kwargs(kwargs, Client.__init__)session_kwargs.update(client_kwargs)with Session(**session_kwargs) as session: return session.request(method, url, **kwargs)

其中url是请求的资源路径;
最后由Session.request执行。这个Session是对应每个sender的一个实例;
其中会不停地向服务器请求资源( _request_resource ),直到返回结果或失败。
上面请求资源的动作,就是从receiver中获取资源并在channel中支付钱的过程。
流程如下:
  1. 封装headers:contract_address,balance,balance_signature,sender_address,receiver_address,open_block //这里的balance表示channel中已经花掉的钱
  2. 发送请求: requests.Session.request(self, method, url, **kwargs)
  3. 根据响应码执行不同的动作:
    3.1 status_code=OK:返回结果;
    3.2 status_code=PAYMENT_REQUIRED:表示需要付费,根据响应结果再进一步处理:
    3.2.1 response.headers.NONEXISTING_CHANNEL channel不存在:sleep后返回成功
    3.2.2 response.headers.INSUF_CONFS 确认信息不足:sleep后返回成功
    3.2.3 response.headers.INVALID_PROOF 无效的证明:sleep后返回成功
    3.2.4 response.headers.CONTRACT_ADDRESS 不存在或和channelManager的合约地址不一致:返回失败
    3.2.5 response.headers.INVALID_AMOUNT 存款数量无效:见下面流程
    3.2.6 其他情况: 支付:见下面流程
  4. 返回response.headers.INVALID_AMOUNT的处理流程:
    4.1 从response.header中获取余额签名BALANCE_SIGNATURE,sender的余额SENDER_BALANCE(channel中已花掉的钱)
    4.2 验证余额证明,即检查channel的sender是否和从余额签名中恢复出的地址一致。
    4.2.1 签名结果就是上面的BALANCE_SIGNATURE
    4.2.2 被签名的数据是
[// type,name,data ('string', 'message_id', 'Sender balance proof signature'), ('address', 'receiver', receiver), ('uint32', 'block_created', (open_block_number, 32)), ('uint192', 'balance', (balance, 192)), ('address', 'contract', contract_address) ]

4.2.3 签名算法是:将type和name组成字符串形式的list,进行keccak256,将data组成list进行keccak256, 两者结果再组合再一次进行keccak256。
4.2.4 根据被签名的数据和签名结果恢复出签名者地址
4.3 如果验证通过,并且response.header中的SENDER_BALANCE和本地保存的sender余额一致,表示: 服务器试图将最后一次未经确认的付款伪装为确认付款,返回失败 。如果余额不一致,表示:服务器提供了不同的channel余额证据,本地采用该最新的余额,本地更新余额和余额签名。
4.4 如果验证不通过,服务器 没有 提供不同的channel余额证据,将本地余额更新为0.
4.5 无论验证是否通过, 都需要进行下面的支付操作。
3.7 客户端中链下支付的处理总流程
  1. 检查当前Sender和Receiver之间的channel是否还存在或者状态是否还是open;不满足者则获取一个新的channel: self.client.get_suitable_channel ,见3.3小节。
  2. 检查当前channel是否足够支付, 不足则需要充值: self.channel.topup(self.topup_deposit(price))
    2.1 检查sender在链上的token余额是否足够充值。
    2.2 发送一个transfer交易,其中的data数据包含了{sender,receiver,blockNumber},合约中的函数会检查data数据的长度,该数据即代表充值。
    2.3 等待充值成功的事件。 ChannelToppedUp
  3. 执行链下支付: self.channel.create_transfer(price)
更新sender在该channel的balance和balance签名。
4. 后端paywalled server proxy服务 后端服务提供付费资源管理及支付通道管理。启动前需要传入下面参数:
  1. 接收者的私钥 (表示该后端只为这一个receiver服务)
  2. 存储balance proof的文件路径,格式是" <管理合约地址前10字节>_<接收者地址前10字节>.db
启动过程如下:
  1. 实例化一个web3: web3 = Web3(HTTPProvider(rpc_provider), 是操作区块链,和合约交互的rpc接口;
  2. 新建一个ChannelManager实例:channel_manager, 管理channel的生命周期;
  3. 创建 app = PaywalledProxy(channel_manager) , 监听客户端的访问,创建后会立即同步链上的数据
付费资源包括静态资源,动态资源,添加方法如下:
4.1 添加静态资源类型 定义URI资源的支持的http方法:
class StaticPriceResource(Expensive): def get(self, url: str, param: str): log.info('Resource requested: {} with param "{}"'.format(request.url, param)) return param

资源的价格是固定的。
添加资源:
app.add_paywalled_resource( cls=StaticPriceResource, url="/echofix/", price=5 )

通过/echofix/foo 就可以获取资源, 只有当支付5个token后,proxy才会返回foo给用户,如果没有支付,则会返回 402 Payment Required
定义URL资源:
app.add_paywalled_resource( cls=PaywalledProxyUrl, url="cdn\/.*", resource_class_kwargs={"domain": 'http://cdn.myhost.com:8000/resource42'} )

【链下支付协议(微雷电microRaiden深入分析)】domain参数指定获取内容的远端URL
4.2 添加动态资源类型
class DynamicPriceResource(Expensive): def get(self, url: str, param: str): log.info('Resource requested: {} with param "{}"'.format(request.url, param)) return paramdef price(self): return len(request.view_args['param'])app.add_paywalled_resource( cls=DynamicPriceResource, url="/echodyn/", )

  • 这里定义了price是和资源的长度相关。比如资源/echodyn/foo 需要支付 3 token。
4.2 启动proxy
app.run(debug=True) app.join()

5 链下支付总体流程 链下支付协议(微雷电microRaiden深入分析)
文章图片
image
  1. sender通过webApp向proxy请求一个付费资源;
  2. proxy返回一个要求付费的响应码:402;
  3. webApp检查本sender是否已经和receiver(proxy)建立过channel,如果已经建立,返回该channel,如果没有,者新建channel后返回该channel;
  4. sender使用该channel发起购买资源的请求;
  5. webApp从合约中检查该channel的余额证明,并要求sender生成一个最新的余额证明,发送给proxy;
  6. proxy验证余额证明(恢复sender地址并比较和发送的sender地址是否一致);
    6.1 channel中的deposit不足, 通知webApp要求追加deposit, 返回402;
    6.2 channel不存在,要求webApp重新建channel,返回402:
  7. 找到对应的channel,进行支付操作:更新channel的balance和签名;
    7.1 channel的余额无效或签名无效,返回402;
    7.2 否则更新余额成功, 返回200;

    推荐阅读