microRaiden是雷电网络的简化版本,是一种低成本、可扩展、低延时的链下微支付解决方案。 他将雷电网络中链下支付网络简化为P2P单向微支付通道, 但保留了快速支付的优势,降低部署难度,简化支付流程。
微雷电基于以太坊开发, 是以太坊的二层支付协议。兼容ERC20/ERC223标准token接口,因此,可以直接将现有已部署在以太坊上的token和微雷电协议对接,而不需要进行token数据的迁移。
1. microRaiden的特点及应用场景
microRaiden是针对N对1 的商店模式而专门优化的一种支付通道,特点如下:
- 支持ERC20/ERC223标准的token;
- 没有支付网络, 只有直连单向支付通道;
- 支付前需要建立支付通道(要花gas);
- 只能由支付者向指定的收款者创建支付通道;
- 接收者可以随时直接关闭通道; 支付者想关闭通道,需要接收者的收款证明;
针对上述特点,以及微雷电设计初衷,它适用于下面的应用场景:
- 接收者地址相对固定
- 发送者建立一条通道后不需要立即拆除通道
- 在同一通道内有较多的支付需求
场景举例:接收者提供很多网络资源(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等几大数据结构,关系如下:
上面的图是官方生成的, 了解一下3个主要结构的主要功能就行:
- Client: 和以太坊智能合约进行交互,包括创建channel等;
- Channel: 管理本地链下channel的数据;
- Session:代表客户端和服务端的一个会话,处理http请求和响应,封装了Client和Channel的操作,是客户端最外层的接口。
客户端主要功能如下:
- 余额证明的签名及地址恢复;
- 链上链下的channel信息同步;
- 管理channel:新建,查询,获取指定channel,channel的deposit追加;
- 和服务端的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信息到本地。 同步流程如下:
- 查询channelManager合约中所有已经执行的交易的事件(包括
ChannelCreated,ChannelToppedUp,ChannelCloseRequested,ChannelSettled
事件),过滤条件是本sender地址。 - 从交易事件中解析出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的接口调用封装。
- 查询和指定receiver的所有已经打开的channel(实际上只有1条), 如果存在并已经打开,并有足够的deposit,返回该channel;如果存在但没有足够的deposit,进行topup后返回该channel
- 如果不存在,则新建channel并返回,新建channel见下面小节
3.4 Client.open_channel 新建一条channel
实现链上channel相关的操作。
- 转账deposit数量的token给channel管理合约地址, transfer的data指定receiver地址(离线签名方式发送)。离线签名的交易通过contract_proxy.token_proxy的
create_signed_transaction
实现,链上的流程间3.4.1小节。 - 监听并等待channel的创建事件。通过contract_proxy.channel_manager_proxy的
get_channel_created_event_blocking
实现 - 监听到创建channel创建成功的事件后,构造Channel结构,保存该channel到Session。
3.4.1 链上创建channel的流程
一句话概括: token转账动作自动触发创建channel
一个sender如果希望和指定的receiver建立一条支付channel,并在这条channel上存款10token,只需要给ChannelContract合约地址转入存款数目的token即可。注意: 所有地址的余额是保存在token合约中的,这里的地址不限于个人账户地址,也可以是普通合约的地址。
创建支付通道是由微雷电客户端发起的,流程如下:
- sender通过webApp输入存款数目10,和receiver的地址;
- webApp向ERC223 token合约发起一笔交易: 即调用该合约的transfer函数,参数为:
2.1 目标地址: 即channels管理合约(ChannelContract)地址;
2.2 目标值:即存款10个token;
2.3 data:自定义数据,这里是receiver的地址; - ERC223 token合约的transfer函数按照标准定义的实现方式,会调用目标地址的tokenFallback函数。也就是说, 当sender向目标地址进行transfer转账时,如果目标地址是合约地址(在本例子中是ChannelsContract合约),则会调用该合约的定义的tokenFallback函数;
- 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
3.5 对一条channel进行充值(追加押金)
充值和创建类似, 只是data数据变成了:
_data = sender_address[2:] + receiver_address[2:] + hex(open_block_number)[2:].zfill(8)
_data = bytes.fromhex(_data)
同样, 兼容erc20的流程如下:
需要2笔交易:approve和topUp
3.6 channel的关闭
sender和receiver都可以发起channel关闭的请求,但是处理流程稍有不同:
- receiver使用sender的余额签名发起关闭channel操作
处理流程如下:
a. 检查余额签名者是不是sender;
b. 对channel进行结算(剩余deposit给sender,支付金额给receiver);
c. 关闭可能存在的sender发起的对该channel的“挑战”; - sender使用receiver的关闭签名发起关闭channel操作
处理流程如下:
a. 检查余额签名者是不是sender;
b. 检查关闭签名者是不是receiver;
c. 对channel进行结算; - sender没有使用receiver的关闭签名发起关闭channel操作
处理流程如下:
a. 检查余额签名者是不是sender
b. 发起一个”挑战“,默认挑战周期是30个block时间;此时channel是没有关闭的,只是记录了该channel被sender发起了一个关闭”挑战“。
有2种情况:- 在挑战期间,receiver使用sender的余额签名发起关闭channel操作;表示channel正常关闭;
- 在“挑战”周期结束后(意味着receiver没有在挑战期间关闭该channel,而且对该关闭没有异议),sender调用settle操作;表示sender对该channel进行结算, 并正常关闭channel;
3.7 Session :一个支持微雷电支付协议的requests包
Session是客户端的核心,通过它可以完成购买资源的操作。
- 创建一个Session实例
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中支付钱的过程。
流程如下:
- 封装headers:contract_address,balance,balance_signature,sender_address,receiver_address,open_block //这里的balance表示channel中已经花掉的钱
- 发送请求:
requests.Session.request(self, method, url, **kwargs)
- 根据响应码执行不同的动作:
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 其他情况: 支付:见下面流程 - 返回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 客户端中链下支付的处理总流程
- 检查当前Sender和Receiver之间的channel是否还存在或者状态是否还是open;不满足者则获取一个新的channel:
self.client.get_suitable_channel
,见3.3小节。 - 检查当前channel是否足够支付, 不足则需要充值:
self.channel.topup(self.topup_deposit(price))
2.1 检查sender在链上的token余额是否足够充值。
2.2 发送一个transfer交易,其中的data数据包含了{sender,receiver,blockNumber},合约中的函数会检查data数据的长度,该数据即代表充值。
2.3 等待充值成功的事件。ChannelToppedUp
- 执行链下支付:
self.channel.create_transfer(price)
更新sender在该channel的balance和balance签名。
4. 后端paywalled server proxy服务
后端服务提供付费资源管理及支付通道管理。启动前需要传入下面参数:
- 接收者的私钥 (表示该后端只为这一个receiver服务)
- 存储balance proof的文件路径,格式是"
<管理合约地址前10字节>_<接收者地址前10字节>.db
启动过程如下:
- 实例化一个web3: web3 = Web3(HTTPProvider(rpc_provider), 是操作区块链,和合约交互的rpc接口;
- 新建一个ChannelManager实例:
channel_manager
, 管理channel的生命周期; - 创建
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 链下支付总体流程
- sender通过webApp向proxy请求一个付费资源;
- proxy返回一个要求付费的响应码:402;
- webApp检查本sender是否已经和receiver(proxy)建立过channel,如果已经建立,返回该channel,如果没有,者新建channel后返回该channel;
- sender使用该channel发起购买资源的请求;
- webApp从合约中检查该channel的余额证明,并要求sender生成一个最新的余额证明,发送给proxy;
- proxy验证余额证明(恢复sender地址并比较和发送的sender地址是否一致);
6.1 channel中的deposit不足, 通知webApp要求追加deposit, 返回402;
6.2 channel不存在,要求webApp重新建channel,返回402: - 找到对应的channel,进行支付操作:更新channel的balance和签名;
7.1 channel的余额无效或签名无效,返回402;
7.2 否则更新余额成功, 返回200;