链下支付协议:微雷电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等几大数据结构,关系如下:

image

上面的图是官方生成的, 了解一下3个主要结构的主要功能就行:

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合约中的,这里的地址不限于个人账户地址,也可以是普通合约的地址。

创建支付通道是由微雷电客户端发起的,流程如下:

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

image

3.5 对一条channel进行充值(追加押金)

充值和创建类似, 只是data数据变成了:

_data = sender_address[2:] + receiver_address[2:] + hex(open_block_number)[2:].zfill(8)
_data = bytes.fromhex(_data)

image

同样, 兼容erc20的流程如下:

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/<string:param>",
        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'}
)

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 param

    def price(self):
            return len(request.view_args['param'])

app.add_paywalled_resource(
    cls=DynamicPriceResource,
    url="/echodyn/<string:param>",
)

  • 这里定义了price是和资源的长度相关。比如资源/echodyn/foo 需要支付 3 token。

4.2 启动proxy

app.run(debug=True)
app.join()

5 链下支付总体流程

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

推荐阅读更多精彩内容