目录 | ||||
---|---|---|---|---|
|
一、需求描述
1、目标
实现基于Move的多方状态通道方案,通过该多方状态通道方案支持多人实时游戏。实现基于Move的多方状态通道方案,通过该方案支持多人实时游戏。
二、需求分析
1、用例图
对于多方状态通道系统,Dapp开发者可以API接口或者Stream接口,创建状态通道,加入状态通道,离开状态通道,关闭状态通道,获取状态通道的信息,给状态通道发送交易和订阅状态通道状态变更。
...
1、总体架构
...
Settlement Chain: 支持结算的链,可以是Starcoin、AptOS、Sui 支持结算的链,可以是AptOS、Sui 或者 RoochRooch。
蓝色的P2P Node: 状态通道的Leader 节点,所有对状态通道的写操作,都需要转发给Leader节点
白色的P2P Node: 状态通道的Follow 状态通道的Follower 节点,从Leader 节点同步最新的交易并验证执行。
...
Dapp 分 Client代码和 Constract代码,Constract代码即可以运行在Settlement Chain上也可以运行在状态通道中。
...
2、如何防止P2PNode作弊
所有参与状态通道的P2P Node 需要抵押一定的原生代币,如果P2P 需要抵押一定的虚拟资产,如果P2P Node 作弊或者不合作。将扣除抵押的原生代币。为了防止每次进入状态通道都质押币,用户可以选择将币质押给状态通道共享质押合约。这样用户进入状态通道时,指定使用共享质押合约作为抵押就可以。当用户的P2P Node出现作弊或者不合作将从共享质押合约中扣币。作弊或者不合作。将扣除抵押的原虚拟资产。为了防止每次进入状态通道都质押资产,用户可以选择将资产质押给状态通道共享质押合约。这样用户进入状态通道时,指定使用共享质押合约作为抵押就可以。当用户的P2P Node出现作弊或者不合作将从共享质押合约中扣出资产。
作弊的场景:
场景 | 1人状态通道 | 两人状态通道 | 三人状态通道 | n人状态通道 |
---|---|---|---|---|
Leader 修改MoveVM实现/合约代码 | ? | Follower 本地运行立即可以发现,然后举报,合约强制Leader退出通道,5分钟内不许再次进入,不罚款 | 任何Follower 本地运行立即可以发现,然后举报,两票赞成后,合约强制Leader退出状态通道,5分钟内不许再次进入,并罚款 | 任何Follower 本地运行立即可以发现,然后举报,两票赞成后,合约强制Leader退出状态通道,5分钟内不许再次进入,并罚款 |
Leader 丢弃/延迟对手的交易 | Follower发现Leader没有回复自己的交易,可以举报,合约关闭状态通道,不罚款 | Follower发现Leader没有回复自己的交易,可以举报,合约关闭状态通道,不罚款 |
...
3、如何选举状态通道的Leader节点
任何状态通道成员可以发起结算提案,一般状态通道的Leader会定时发起结算提案,有成员要离开状态通道时也会发起结算提案。结算提案包括结算的起始高度、结算高度、状态通道的变更集和发起方的签名。发起方创建结算提案后,需要通知所有Peer对提案进行投票,Peer收到通知后会使用本地交易数据对提案进行验证,验证的逻辑是计算起始高度和结算高度的变更集是否和提案中的变更集一致,如果一致认为提案没有问题,可以投赞成票,如果不一致投反对票。当发起方收到2/3的赞成票后,发起方执行提案,完成状态通道的结算。状态通道结算完成后,所有成员可以生成结算高度的快照,并清理结算高度之前的交易数据。
4、如何选举状态通道的Leader节点
所有加入状态通道的成员,需要和合约保持心跳,15s内有心跳的成员认为是有效成员。所有加入状态通道的成员,需要和合约保持心跳,15s内有心跳的成员认为是有效成员。
当状态通道小于等于5个成员:
第一个进入状态通道的成员,查询状态通道状态,没有Leader, 将调用合约方法申请成为Leader, 合约检查只有1个有效成员,将验证通过。
其他成员进入状态通道的成员,查询状态通道状态,发现有Leader, 他将从Leader同步状态,并和Leader保持心跳, 如果和Leader15s心跳没有响应,他调用状态通道合约申请自己为Leader如果和Leader 15s 心跳没有响应,他调用状态通道合约申请自己为Leader, 合约检查之前的Leader如果有效将拒绝申请,如果之前的leader无效了,将通过申请。
...
对应申请失败的成员,将再次查询状态通道的最新状态,并和最新的Leader保持状态同步。
当状态通道大于5个成员:
需要选举5个成员作为候选成员,其他成员需要将投票权委托给候选成员,每个候选成员最大只能接受n/5-1个其他成员的委托。候选成员需要提供代理连接Leader节点功能,其他成员优先通过候选成员和Leader通讯。
问题:
选举过程中状态通道不可写。选举过程中状态通道不可写?
Leader故障如何快速切换?
...
4、如何选择和Leader的最优通信路径问题
状态通道的Follower需要从Leader同步状态和转发交易,但是有可能Follower直连Leader响应延迟没有经其他Follower中转效率高,所以为了发现最优路径。所有的和Peer的心跳包需要带上和Leader通信的最优延迟。
...
那么Follower1 将选择经过Follower 和 Leader通信。
5、如何加载状态
首先Leader从链上状态通道合约中获取状态通道的代码和初始账号状态。为了防止Leader加载过程中有其他成员加入,每个加入成员需要分配一个加入序号,Leader需要记录他加载时的最大序号,Leader加载成功后,其他成员从Leader同步初始状态,并也从链上加载状态通道状态并执行验证。当有新成员加入时,加入序号加1,有Leader验证后并同步给其他成员节点。
6、如何结算状态
任何状态通道成员可以发起结算提案,一般状态通道的Leader会定时发起结算提案,有成员要离开状态通道时也会发起结算提案。结算提案包括结算的起始高度、结算高度、状态通道的变更集和发起方的签名。发起方创建结算提案后,需要通知所有Peer对提案进行投票,Peer收到通知后会使用本地交易数据对提案进行验证,验证的逻辑是计算起始高度和结算高度的变更集是否和提案中的变更集一致,如果一致认为提案没有问题,可以投赞成票,如果不一致投反对票。当发起方收到2/3的赞成票后,发起方执行提案,完成状态通道的结算。状态通道结算完成后,所有成员可以生成结算高度的快照,并清理结算高度之前的交易数据。
四、详细设计
1、合约设计
状态通道的合约包括两部分,一部分给Dapp的合约调用,另一部分给P2P Node调用。状态通道的合约包括两部分,一部分给Dapp合约调用的合约接口,另一部分给P2P Node调用的合约接口。
1.1 状态通道合约类图
...
示例使用示例
协同编辑器合约类图:协同编辑器合约示例:
...
代码块 |
---|
module rooch_demo::editor { use std::string::{String, utf8}; use aptos_std::table::{Self, Table}; use aptos_framework::timestamp; const EDITOR_ADDRESS:address = @rooch_demo; const ERR_NOT_CONTRACT_OWNER: u64 = 0; const DRIVE_ALREADY_REGISTERED: u64 = 1; const ERR_ALREADY_INITIALIZED: u64 = 2; struct File has store, copy, drop { id: u64, name: String, content_type: vector<u8>, content_hash: vector<u8>, owner: address, create_time: u64, last_update_time: u64, } struct Folder has store, copy, drop { id: u64, name: String, file_ids: vector<u64>, folder_ids: vector<u64>, owner: address, create_time: u64, last_update_time: u64, } struct Drive has key, store { name: String, files: Table<u64, File>, folders: Table<u64, Folder>, root_folder_id: u64, next_file_id: u64, private: bool, public_key: String, owner: address, } struct Element has store, copy, drop { id: u128, type: u16, attributes: SimpleMap<String, String>, children_ids: vector<u64>, text: String, } struct Document has store, copy, drop { drive_address: u64, file: File, title: vector<u8>, elements: Table<u64, Element>, root_element_id: u64 } struct EditingDocument store { drive_address: address, file_id: u64, state_channel_id: u64 } struct DocumentEditor has store { editingDocuments: Table<u64, EditingDoc<Document>> } public fun initialize(account: &signer) { let account_addr = Signer::address_of(account); assert!(account_addr==EDITOR_ADDRESS, Errors::requires_address(ERR_NOT_CONTRACT_OWNER)); assert!(!exists<DocumentEditor>(account_addr), Errors::already_published(ERR_ALREADY_INITIALIZED)); move_to(account, DocumentEditor{ editingDocuments: table::new<u64, EditingDoc<Document>>(), }) } public fun drive_register(account: &signer, name: vector<u8>, public_key: vector<u8>, private: bool) { let addr = signer::address_of(account); assert!(!exists<Server>(addr), error::already_exists(DRIVE_ALREADY_REGISTERED)); let drive = Drive { name: utf8(name), files: table::new<u64, File>(), folders: table::new<u64, Folder>(), root_folder_id: 0, next_file_id: 0, private: private, public_key: public_key, owner: account, }; let rootFolder = Folder { id: 0, name: utf8(b'/'), files: vector::empty<u64>(), folders : vector::empty<u64>(), owner: account, create_time: timestamp::now_microseconds(), last_update_time: 0, } vector::append<address>(&mut drive.folders, rootFolder); drive.root_folder_id = rootFolder.id; drive.next_file_id = drive.next_file_id + 1; move_to(account, drive); } } |
聊天合约类图:聊天合约示例:
...
代码块 |
---|
module rooch_demo::chat { use std::vector; use std::string::{String, utf8}; use aptos_std::table::{Self, Table}; use rooch::state_channel::{Self, StateChannel}; const SERVER_ALREADY_REGISTERED: u64 = 0; const CHAT_ACCOUNT_ALREADY_REGISTERED: u64 = 1; struct Message has store { id: u64, type: u16, props: SimpleMap<String, String> content: String, publish_time: u64 } struct ChatGroup has store { id: u64, name: String, admins: vector<address> messages: Table<u64, Message>, server_id: u64, public_key: String, create_time: u64, } struct ChatSession has store,copy,drop { chat_group_id: u64, state_channel_id: u64 } struct Server has key { name: String, chatGroups: vector<ChatGroup>, chatSessions: Table<u64, ChatSession>, admins: vector<address>, next_chat_group_id: u64, } struct ChatAccount has key { name: String, groups: Table<u64, PrivateKey>, sessions: vector<ChatSession>, } public fun server_register(account: &signer, name: vector<u8>) { let addr = signer::address_of(account); assert!(!exists<Server>(addr), error::already_exists(SERVER_ALREADY_REGISTERED)); let server = Server { name: utf8(name), chatGroups: vector::empty<ChatGroup>(), chatSessions: table::new<u64, ChatSession>(), admins: vector::empty<address>(), next_chat_group_id: 0, }; vector::append<address>(&mut server.admins, addr); move_to(account, server); } public fun chat_account_register(account: &signer, name: vector<u8>) { let addr = signer::address_of(account); assert!(!exists<ChatAccount>(addr), error::already_exists(CHAT_ACCOUNT_ALREADY_REGISTERED)); let chat_account = ChatAccount{ name: utf8(name), groups: table::new<u64, PrivateKey>(), sessions: table::new<u64, ChatSession>(), }; move_to(account, chat_account); } } |
MoveCraft合约类图:MoveCraft合约示例:
...
代码块 |
---|
module rooch_demo::movecraft { use std::vector; use std::string::{String, utf8}; use aptos_std::table::{Self, Table}; use rooch::state_channel::{Self, StateChannel}; const SERVER_ALREADY_REGISTERED: u64 = 0; const CHAT_ACCOUNT_ALREADY_REGISTERED: u64 = 1; struct Vector3 has store,copy,drop { x: u128, y: u128, z: u128, } struct Position has store,copy,drop { x: u128, y: u128, } struct Rectangle has store,copy,drop { top_left: Position, bottom_right: Position, } struct Block has store { id: u64, type: u16, props: SimpleMap<String, String>, create_time: u64 } struct Land has store { id: u64, name: String, boundary: vector<Rectangle> messages: Table<u64, Message>, world_address: address, owner: address, create_time: u64, } struct Inventory has store { blocks: SimpleMap<u16, vector<Block>>, } struct LandSession has store,copy,drop { world_address: address, land_id: u64, channel_id: u64, create_time: u64, } struct World has key { name: String, type: u8, seed: u128, blocks: Table<Vector3, Block>, lands: Table<u64, Land>, sessions: vector<LandSession>, next_chat_group_id: u64, } struct GameAccount has key { name: String, sessions: vector<LandSession>, } public fun world_register(account: &signer, name: vector<u8>) { let addr = signer::address_of(account); assert!(!exists<Server>(addr), error::already_exists(SERVER_ALREADY_REGISTERED)); let server = Server { name: utf8(name), chatGroups: vector::empty<ChatGroup>(), chatSessions: table::new<u64, ChatSession>(), admins: vector::empty<address>(), next_chat_group_id: 0, }; vector::append<address>(&mut server.admins, addr); move_to(account, server); } public fun game_account_register(account: &signer, name: vector<u8>) { let addr = signer::address_of(account); assert!(!exists<ChatAccount>(addr), error::already_exists(CHAT_ACCOUNT_ALREADY_REGISTERED)); let chat_account = ChatAccount{ name: utf8(name), groups: table::new<u64, PrivateKey>(), sessions: table::new<u64, ChatSession>(), }; move_to(account, chat_account); } } |
...
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型 |
参数:
参数名称 | 类型 | 描述 |
---|---|---|
init_state |
| 初始状态 |
config | Config | 状态通道配置 |
返回值:
返回值 | 类型 | 能力 | 描述 |
---|---|---|---|
state_channel_id | u256u64 | 状态通道ID |
业务逻辑:
使用一个自定义的初始状态来创建一个状态通道。状态通道的合约代码通过状态的模块自动提取。
示例:使用示例:
协同编辑器创建状态通道协同编辑器,创建状态通道
代码块 |
---|
module rooch_demo::editor { use roochaptos_framework::stateaptos_channelcoin::{Self, StateChannelAptosCoin}; use rooch::state_channel::{Self, StateChannel}; const ERR_FILE_NOT_EXISTS: u64 = 5; const ERR_FILE_ALREADY_OPEN: u64 = 6; public entry fun document_editor_open_document(account: &signer, drive_address: address, file_id: u64) { assert!(drive_file_exists(drive_address, file_id), Errors::requires_address(ERR_FILE_NOT_EXISTS)); let editor = borrow_global_mut<DocumentEditor>(EDITOR_ADDRESS); let doc = document_create(account, assert!(!table::contains(editor.editingDocuments, (drive_address, file_id) let ch_id = state_channel::create<Document>(doc), Errors::requires_address(ERR_FILE_ALREADY_OPEN)); let editing_doc = EditingDocument{ document_create(account, drive_address, file_id) let cfg drive_address: drive_address, = state_channel::create_config(1); //加入状态通道需要质押1个AptosCoin let ch_id = filestate_id: file_id,channel::create<Document,(),AptosCoin>(doc, cfg); let state_channel_id: ch_id editing_doc = EditingDocument{ } drive_address: drive_address, save_editing_doc(editor, editing_doc)file_id: file_id, } } state_channel_id: ch_id } table::upser(editor.editingDocuments, (drive_address, file_id), editing_doc) } } |
聊天示例:
代码块 |
---|
module rooch_demo::chat { use std::vector; use std::string::{String, utf8}; use aptos_framework::aptos_coin::{Self, AptosCoin}; use rooch::state_channel::{Self, StateChannel}; public fun chat_session_create(account: &signer, server_address: address, chat_group_id: u64) { let addr = signer::address_of(account); let server = borrow_global_mut<Server>(server_address); let chat_account = borrow_global_mut<ChatAccount>(addr); let chat_group = server_borrowwithdraw_chat_group(server, chat_group_id); let cfg = state_channel::create_config(0); //加入状态通道需要质押0个AptosCoin let ch_id = state_channel::create<ChatGroup>create<ChatGroup,(),AptosCoin>(chat_group, cfg); let session = ChatSession{ server_address: server_address, chat_group_id: chat_group_id, state_channel_id: ch_id } server_add_chat_session(server, copy session) chat_account_add_chat_session(chat_account, copy session) } } |
...
代码块 |
---|
module rooch_demo::movecraft { use std::vector; use std::string::{String, utf8}; use aptos_framework::aptos_coin::{Self, AptosCoin}; use rooch::state_channel::{Self, StateChannel}; public fun land_session_create(account: &signer, world_address: address, land_id: u64) { let addr = signer::address_of(account); let world = borrow_global_mut<World>(world_address); let game_account = borrow_global_mut<GameAccount>(addr); let land = world_borrowwithdraw_land(serverworld, land_id); let ch_idcfg = state_channel::create<Land>create_config(land1); //加入状态通道需要质押1个AptosCoin let (settle_cap, let session = LandSession{ch_id) = state_channel::create<Land,(Hero,Inventory),AptosCoin>(land, cfg); let session = LandSession{ world_address: world_address, land_id: land_id, state_channel_id: ch_id, settle_cap: settle_cap, } world_add_land_session(world, copy session) game_account_add_land_session(game_account, copy session) } } |
...
代码块 |
---|
rooch::state_channel::join |
类型参数:
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型资产类型 |
参数:
参数名称 | 类型 | 描述 | |||
---|---|---|---|---|---|
sender | &signer | 发起方 | |||
state_channel_id |
| 状态通道ID | deposit | Token<TokenType> | 质押Token |
assets | Assets | 带入状态通道的资产 |
事件:
事件名称 | 事件数据 | 描述 |
---|---|---|
state_channel_join_event | { “member “state_channel_address“id“: “vector<u8>“u64, “input “member_assets“address“: Assetsaddress, } | 加入状态通道事件 |
返回值:
无
业务逻辑:
加入某个状态通道,指定需要带入的资产加入某个状态通道,指定需要带入的资产。自动扣除押金。如果押金余额不够将加入失败。
示例:
协同编辑器,加入状态通道
代码块 |
---|
module rooch_demo::editor { use rooch::state_channel::{Self, StateChannel}; const ERR_FILE_NOT_EXISTS: u64 = 5; public fun editing_document_join(account: &signer, docdrive_idaddress: u64) address, file_id: u64) { let editor = borrow_global_mut<DocumentEditor>(EDITOR_ADDRESS); let editing_doc = table::borrow<u64, EditingDocument>(&editor.editingDocuments, doc(drive_address, file_id)) state_channel::joinjoin<()>(account, editing_doc.channel_id, ()); } } |
聊天合约,加入群聊
代码块 |
---|
module rooch_demo::chat { use std::vector; use std::string::{String, utf8}; use rooch::state_channel::{Self, StateChannel}; public fun chat_session_join(account: &signer, chat_group_id: u64) { let addr = signer::address_of(account); let chat_account = borrow_global_mut<ChatAccount>(addr); let session = chat_account_borrow_chat_session(chat_account, chat_group_id) state_channel::join<ChatGroup>join<()>(account, session.state_channel_id,()); } } |
MoveCraft合约,加入地块
代码块 |
---|
module rooch_demo::movecraft { use std::vector; use std::string::{String, utf8}; use rooch::state_channel::{Self, StateChannel}; public fun land_session_join(account: &signer, land_id: u64) { let addr = signer::address_of(account); let game_account = borrow_global_mut<GameAccount>(addr); let session = game_account_borrow_session(game_account, land_id); let hero = game_account_withdraw_hero(game_account); let inventory = game_account_getwithdraw_inventory(game_account); state_channel::join<Inventory>join<(Hero, Inventory)>(account, session.state_channel_id, (hero, inventory)); } } |
1.2.3 离开状态通道
代码块 |
---|
leave_rooch::state_channel::leave |
类型参数:
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型 |
...
参数名称 | 类型 | 描述 |
---|---|---|
sender | signer | 发起方 |
state_channel_id |
| 状态通道ID |
事件:
事件名称 | 事件数据 | 描述 |
---|---|---|
leave_state_channel_leave_event | { “member_address“: “vector<u8>“, } | 离开状态通道事件 |
...
离开某个状态通道,同时触发状态通道结算,其他用户可以继续使用该状态通道。
...
示例:
协同编辑器,离开状态通道
代码块 |
---|
close_state_channel |
类型参数:
...
类型参数
...
约束
...
描述
...
StateT
...
store
...
状态类型
参数:
...
参数名称
...
类型
...
描述
...
sender
...
signer
...
发起方
...
state_channel_id
...
u256
...
状态通道ID
事件:
...
事件名称
...
事件数据
...
描述
...
close_state_channel_event
{
}
...
关闭状态通道事件
返回值:
无
业务逻辑:
关闭某个状态通道,同时触发状态通道结算。
1.3 给P2P Node的接口
1.3.1 保持心跳
代码块 |
---|
keep_alive_state_channel_entry |
类型参数:
...
类型参数
...
约束
...
描述
...
StateT
...
store
...
状态类型
参数:
...
参数名称
...
类型
...
描述
...
sender
...
signer
...
发起方
...
state_channel_id
...
u256
...
状态通道ID
...
peer_delays
...
[]PeerDelay
...
和其他节点的延迟
返回值:
无
业务逻辑:
关闭某个状态通道,同时触发状态通道结算。
1.3.2 创建提案
代码块 |
---|
create_proposal_entry |
类型参数:
...
类型参数
...
约束
...
描述
...
StateT
...
store
...
状态类型
...
ActionT
...
copy, store
...
提案的Action
参数:
...
参数名称
...
类型
...
描述
...
sender
...
signer
...
发起方
...
state_channel_id
...
u256
...
状态通道ID
...
action
...
ActionT
...
提案的动作
返回值:
无
业务逻辑:
发起一个提案。
支持的提案:
...
提案Action
...
提案描述
...
SettlementAction
...
结算
...
PunishAction
...
惩罚
1.3.3 对提案进行投票
代码块 |
---|
vote_proposal_entry |
类型参数:
...
类型参数
...
约束
...
描述
...
StateT
...
store
...
状态类型
参数:
...
参数名称
...
类型
...
描述
...
sender
...
signer
...
发起方
...
state_channel_id
...
u256
...
状态通道ID
...
proposal_id
...
u256
...
提案ID
...
vote_option
...
u8
...
投票选项:
0:反对
1: 赞成
2:弃权
返回值:
无
业务逻辑:
对提案投票。
1.3.4 执行提案
代码块 |
---|
execute_proposal_entry |
类型参数:
...
类型参数
...
约束
...
描述
...
StateT
...
store
...
状态类型
参数:
...
参数名称
...
类型
...
描述
...
sender
...
signer
...
发起方
...
state_channel_id
...
u256
...
状态通道ID
...
proposal_id
...
u256
...
提案ID
返回值:
无
业务逻辑:
...
module rooch_demo::editor {
use rooch::state_channel::{Self, StateChannel};
const ERR_FILE_NOT_EXISTS: u64 = 5;
public fun editing_document_leave(account: &signer, doc_id: u64) {
let editor = borrow_global_mut<DocumentEditor>(EDITOR_ADDRESS);
let editing_doc = table::borrow<u64, EditingDocument>(&editor.editingDocuments, doc_id)
state_channel::leave<()>(account, editing_doc.channel_id);
}
} |
聊天合约,离开群聊
代码块 |
---|
module rooch_demo::chat {
use std::vector;
use std::string::{String, utf8};
use rooch::state_channel::{Self, StateChannel};
public fun chat_session_leave(account: &signer, chat_group_id: u64) {
let addr = signer::address_of(account);
let chat_account = borrow_global_mut<ChatAccount>(addr);
let session = chat_account_borrow_chat_session(chat_account, chat_group_id)
state_channel::leave<()>(account, session.state_channel_id);
}
} |
MoveCraft合约,离开地块
代码块 |
---|
module rooch_demo::movecraft {
use std::vector;
use std::string::{String, utf8};
use rooch::state_channel::{Self, StateChannel};
public fun land_session_leave(account: &signer, land_id: u64) {
let addr = signer::address_of(account);
let game_account = borrow_global_mut<GameAccount>(addr);
let session = game_account_borrow_session(game_account, land_id);
let (hero, inventory) = state_channel::leave<(Hero, Inventory)>(account, session.state_channel_id);
game_account_deposit_hero(game_account, hero)
game_account_deposit_inventory(game_account, inventory)
}
} |
1.2.4 关闭状态通道
代码块 |
---|
rooch::state_channel::close |
类型参数:
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型 |
参数:
参数名称 | 类型 | 描述 |
---|---|---|
sender | signer | 发起方 |
state_channel_id |
| 状态通道ID |
事件:
事件名称 | 事件数据 | 描述 |
---|---|---|
close_state_channel_event | { } | 关闭状态通道事件 |
返回值:
无
业务逻辑:
关闭某个状态通道,同时触发状态通道结算。
示例:
协同编辑器,关闭状态通道
代码块 |
---|
module rooch_demo::editor {
use rooch::state_channel::{Self, StateChannel};
const ERR_FILE_NOT_EXISTS: u64 = 5;
public fun editing_document_close(account: &signer, doc_id: u64) {
let editor = borrow_global_mut<DocumentEditor>(EDITOR_ADDRESS);
let editing_doc = table::borrow<u64, EditingDocument>(&editor.editingDocuments, doc_id)
let doc = state_channel::close<Document>(account, editing_doc.channel_id);
editing_document_destroy(editing_doc)
document_destroy(doc)
}
} |
聊天合约,关闭群聊
代码块 |
---|
module rooch_demo::chat {
use std::vector;
use std::string::{String, utf8};
use rooch::state_channel::{Self, StateChannel};
public fun chat_session_close(account: &signer, chat_group_id: u64) {
let addr = signer::address_of(account);
let chat_account = borrow_global_mut<ChatAccount>(addr);
let session = chat_account_borrow_chat_session(chat_account, chat_group_id)
let chat_group = state_channel::close<ChatGroup>(account, session.state_channel_id);
chat_account_destroy(session)
server_deposit_chat_group(server, chat_group)
}
} |
MoveCraft合约,关闭地块
代码块 |
---|
module rooch_demo::movecraft {
use std::vector;
use std::string::{String, utf8};
use rooch::state_channel::{Self, StateChannel};
public fun land_session_close(account: &signer, land_id: u64) {
let addr = signer::address_of(account);
let game_account = borrow_global_mut<GameAccount>(addr);
let session = world_borrow_session(game_account, land_id);
let land = state_channel::close<Land>(account, session.state_channel_id);
world_destroy_session(world, session)
world_deposit_land(game_account, land)
}
} |
1.3 给P2P Node的接口
1.3.1 保持心跳
代码块 |
---|
rooch::state_channel::keep_alive_entry |
类型参数:
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型 |
参数:
参数名称 | 类型 | 描述 |
---|---|---|
sender | &signer | 发起方 |
state_channel_id |
| 状态通道ID |
返回值:
无
业务逻辑:
更新状态通道成员的, lastAliveTime
调用示例:
协同编辑器
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::keep_alive_entry \
--type-args=rooch_demo::editor::Document \
--args=1001 |
聊天合约
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::keep_alive_entry \
--type-args=rooch_demo::chat::ChatGroup \
--args=1001 |
MoveCraft合约
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::keep_alive_entry \
--type-args=rooch_demo::movecraft::Land \
--args=1001 |
1.3.2 创建提案
代码块 |
---|
rooch::state_channel::create_proposal_entry |
类型参数:
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型 |
参数:
参数名称 | 类型 | 描述 |
---|---|---|
sender | signer | 发起方 |
state_channel_id |
| 状态通道ID |
proposal_type | u8 | 提案类型 |
proposal_data | vector<u8> | 提案数据 |
当 proposal_type == 1 时,表示结算提案
proposal_data 数据格式:
代码块 |
---|
{
"from_height": 1001,
"to_height": 1009,
"change_sets": {
"accounts": {
"0x6c31f522bb1bdc6c625f5a39ce4d8c95": {
"rooch_demo::editor::Document": {
`new`:[],
`modify`: ['old_doc_DATA', 'new_doc_DATA'],
`delete`: [],
}
},
"0xf1e8acab0eb5d19288fa1f68167bdee2": {
"0x1::token::Token<0x1::STC::STC>": {
`new`:[],
`modify`: [],
`delete`: [true],
}
},
}
}
} |
当 proposal_type == 2 时,表示惩罚提案
proposal_data 数据格式:
代码块 |
---|
{
"target_member": "0x6c31f522bb1bdc6c625f5a39ce4d8c95",
"height": 1009,
"rawTransaction": "0x6c31f522bb1bdc6c625f5a39ce4d8c933228877666626363333333AB",
"change_sets": {
"accounts": {
"0x6c31f522bb1bdc6c625f5a39ce4d8c95": {
"rooch_demo::editor::Document": {
`new`:[],
`modify`: ['old_doc_DATA', 'new_doc_DATA'],
`delete`: [],
}
},
"0xf1e8acab0eb5d19288fa1f68167bdee2": {
"0x1::token::Token<0x1::STC::STC>": {
`new`:[],
`modify`: [],
`delete`: [true],
}
},
}
}
} |
返回值:
无
业务逻辑:
发起提案。根据提案类型创建不同的提案。
调用示例:
协同编辑器
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::create_settlement_proposal_entry \
--type-args=rooch_demo::editor::Document \
--args=1001 1 b"23A145AB448872B1" |
聊天合约
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::create_settlement_proposal_entry \
--type-args=rooch_demo::chat::ChatGroup \
--args=1001 1 b"23A145AB448872B1" |
MoveCraft合约
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::create_settlement_proposal_entry \
--type-args=rooch_demo::movecraft::Land \
--args=1001 1 b"23A145AB448872B1" |
1.3.3 对提案进行投票
代码块 |
---|
rooch::state_channel:vote_proposal_entry |
类型参数:
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型 |
参数:
参数名称 | 类型 | 描述 |
---|---|---|
sender | signer | 发起方 |
state_channel_id |
| 状态通道ID |
proposal_id | u64 | 提案ID |
vote_option | u8 | 投票选项: 0:反对 1: 赞成 2:弃权 |
返回值:
无
业务逻辑:
对提案投票。
调用示例:
协同编辑器
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::vote_proposal_entry \
--type-args=rooch_demo::editor::Document \
--args=1001 1 1 |
聊天合约
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::vote_proposal_entry \
--type-args=rooch_demo::chat::ChatGroup \
--args=1001 1 1 |
MoveCraft合约
代码块 |
---|
aptos move run \
--function-id rooch::state_channel::vote_proposal_entry \
--type-args=rooch_demo::movecraft::Land \
--args=1001 1 1 |
1.3.4 执行提案
代码块 |
---|
rooch::state_channel:execute_proposal |
类型参数:
类型参数 | 约束 | 描述 |
---|---|---|
| store | 状态类型 |
ProposalAction | store | 提案Action |
参数:
参数名称 | 类型 | 描述 |
---|---|---|
sender | signer | 发起方 |
state_channel_id |
| 状态通道ID |
proposal_id | u256 | 提案ID |
返回值:
ProposalAction
业务逻辑:
执行提案,检查提案是否投票通过,如果投票通过返回提案Action。
调用示例:
协同编辑器
代码块 |
---|
module rooch_demo::editor {
use rooch::state_channel::{Self, StateChannel};
const ERR_FILE_NOT_EXISTS: u64 = 5;
public entry fun document_settlement<Document>(account: &signer, state_channel_id: u64, proposal_id: u64) {
let editor = borrow_global_mut<DocumentEditor>(EDITOR_ADDRESS);
let editing_doc = table::borrow<u64, EditingDocument>(&editor.editingDocuments, state_channel_id);
let settlement_cap = editing_doc.settlement_cap;
let doc = state_channel::borrow_mut<Document>(account, settlement_cap, state_channel_id);
let settlement_action = state_channel::execute_proposal<SettlementAction>(editing_doc.channel_id, proposal_id)
let change_sets = state_channel::get_account_change_sets(settlement_action, @rooch_demo)
let doc_op = state_channel::get_change_set_op<Document>(change_sets)
document_apply_change_set(doc, doc_op);
}
fun document_apply_change_set(doc: &mut Docuemnt, cs: state_channel::StateChangeSet) {
if (op::is_modify(cs)) {
let modify_doc = op::modify(cs)
document_update_element(doc, modify_doc)
} else if (op::is_delete(cs)) {
document_destroy(doc)
}
}
} |
聊天合约
代码块 |
---|
module rooch_demo::chat {
use rooch::state_channel::{Self, StateChannel};
const ERR_FILE_NOT_EXISTS: u64 = 5;
public entry fun chat_group_settlement<ChatGroup>(account: &signer, state_channel_id: u64, proposal_id: u64) {
let chat_server = state_channel::get_config_label(state_channel_id, "server_address");
let server = borrow_global_mut<Server>(chat_server);
let chat_session = server_borrow_chat_session(state_channel_id);
let settlement_cap = chat_session.settlement_cap;
let chat_group = state_channel::borrow_mut<ChatGroup>(account, settlement_cap, state_channel_id);
let settlement_action = state_channel::execute_proposal(state_channel_id, proposal_id)
let account_change_sets = state_channel::get_change_sets(settlement_action, @rooch_demo)
if table::contains(account_change_sets, "rooch_demo::chat::ChatGroup") {
let op = table::borrow_mut(account_change_sets, "rooch_demo::chat::ChatGroup");
chat_group_apply_op(chat_group, op)
}
}
fun chat_group_apply_op(chat_group: &mut ChatGroup, cs: op::OP<vector<u8>>) {
if (op::is_modify(cs)) {
let modify_data = op::modify(cs)
chat_group_update(chat_group, modify_data)
} else if(op::is_delete(cs)){
chat_group_destroy(chat_group)
}
}
} |
MoveCraft合约
代码块 |
---|
module rooch_demo::movecraft {
use rooch::state_channel::{Self, StateChannel};
const ERR_FILE_NOT_EXISTS: u64 = 5;
public entry fun land_settlement<ChatGroup>(account: &signer, state_channel_id: u64, proposal_id: u64) {
let world = state_channel::get_label(state_channel_id, "world_address");
let land_session = world_borrow_land_session(state_channel_id);
let settlement_cap = land_session.settlement_cap;
let land = state_channel::borrow_mut<Land>(account, settlement_cap, state_channel_id);
let settlement_action = state_channel::execute_proposal(state_channel_id, proposal_id)
let change_sets = state_channel::get_account_change_sets(settlement_action, @rooch_demo)
if table::contains(change_sets, "rooch_demo::chat::Land") {
let op = table::borrow_mut(change_sets, "rooch_demo::chat::Land");
land_apply_op(land, op)
}
}
fun land_apply_op(land: &mut Land, cs: op::OP<vector<u8>>) {
if (op::is_modify(cs)) {
let modify_data = op::modify(cs)
land_update(land, modify_data)
}
}
} |
2、P2P Node
2.1 模块图
...
2.2
...
Stream模块
Steam模块,维护和SDK的双向流连接,并转发SDK的请求给对应的状态通道实例。
2.2.1 加入状态通道
命令:state_channel.join
...
参数 | 类型 | 描述 |
---|---|---|
sender | string | 用户钱包地址 |
state_channel_id | string | 状态通道ID |
错误码:
错误码 | 描述 |
---|---|
100001 | 状态通道不存在 |
2.2.3
...
调用状态通道合约函数
命令:state_channel.call
参数:
参数 | 类型 | 描述 |
---|---|---|
sender | string | 用户钱包地址 |
state_channel_id | string | 状态通道ID |
function | string | 函数名称 |
ty_args | []string | 泛型参数 |
args | []string | 参数 |
...
参数 | 类型 | 描述 |
---|---|---|
sender | string | 用户钱包地址 |
state_channel_id | string | 状态通道ID |
错误码:
错误码 | 描述 |
---|---|
100001 | 状态通道不存在 |
返回值:
状态通道状态的快照。
2.3 状态通道
状态通道模块,负责状态通道的状态的分发和验证。
2.3.1 Controller
状态通道控制器,负责处理Sessions模块和Peers模块发送过来的请求。对于游戏类应用主动产生Tick交易。
2.3.1.1 初始化状态通道
业务逻辑:
当状态通道不存在时,调用状态通道合约接口,获取合约详细信息,检查 自己是不是 当状态通道不存在时,调用状态通道合约接口,获取合约详细信息,检查自己是不是 Leader ,
如果是Leader:
就加载初始状态到MemStore,合约代码到 MoveVM,然后监听 state_channel.call 消息,如果有消息就执行合约调用,把调用产生的副作用,广播给其他 Peer 节点和连接的所有客户端。设置一个定时,触发Tick,运行合约的 OnTick 函数,如果有副作用产生就广播给Peer节点和连接的所有客户端。
如果不是Leader如果是Follower:
和Leader Peer 建立连接, 然后从Leader Peer 获取通道的初始状态和合约代码。然后监听Leader发送过来的消息。如何是合约执行结果消息,就在本地的MoveVM中执行,如果执行产生的副作用和Leader一致,就广播给客户端。如果不一致,就调用主链的合约创建一个提案,惩罚Leader, 并请求其他Peer投票,如果收到的投票数足够,就执行提案惩罚Leader,获取罚金。
如果状态通道配置有Tick函数和间隔:
配置定时器,触发Tick交易。
2.3.1.2 处理加入状态通道请求
业务逻辑:
如果是Leader:
当某个SDK发送加入状态通道请求时,需要检查链上状态通道是否存在该成员地址,如果不存在拒绝加入请求。如果存在,创建成员加入交易,并将执行结果和交易一起广播给其他Peer。
如果是Follower:
当从Leader收到加入状态通道请求,从链上获取成员状态和带入的资产,验证交易是否合法。如果合法更新本地MoveSandbox状态,如果不合法,发起惩罚Leader的提案。
2.3.1.3 处理离开事件/
...
离开状态通道请求
业务逻辑:
当调用合约或者调用Tick函数,如果返回的事件包括离开状态通道事件 或者 用户主动发起离开状态通道API调用:就发起提案结算状态通道的状态,投票达标后执行状态结算,结算成功后通知对应的Peer下线。
2.3.1.
...
4 处理关闭事件/
...
关闭状态通道请求
业务逻辑:
当调用合约或者调用Tick函数,如果返回的事件包括关闭状态通道事件 或者 用户主动发起关闭状态通道API调用:就发起提案结算状态通道的状态,投票达标后执行状态结算,结算成功后通知所有的Peer下线。
2.3.1.
...
5 处理合约调用请求
业务逻辑:
当用户调用合约时,判断自己是否为Leader, 如果为Leader, 在本地执行合约调用,将结果广播给其他Peers, 然后保存到本地Store. 如果自己不是Leader, 将合约调用转发给Leader.
2.3.1.
...
6 处理状态订阅
业务逻辑:当用户请求订阅状态时,从本地获取快照返回给调用方,同时从快照高度开始从本地Store获取交易,在MoveVM中执行,并将执行产生的状态变更发送给调用方。当从Leader接受到新的交易时,在MoveVM中运行,和Leader的结果对比,如果相同,则将变更转发给调用方,如果不相同发起举报投票。
当用户请求订阅状态时,从本地获取快照返回给调用方,同时从快照高度开始从本地Store获取交易,在MoveVM中执行,并将执行产生的状态变更发送给调用方,如果已经运行过,直接返回状态变更。当从Leader接受到新的交易时,在MoveVM中运行,和Leader的结果对比,如果相同,则将变更转发给调用方,如果不相同发起举报投票。
2.3.1.7 处理投票请求
业务逻辑:
当收到其他成员发送的请求投票请求,
对于结算提案的投票:
首先验证投票的内容是否和本地的MoveSandbox状态一致,如果一致投赞成票,如果不一致投反对票。
对于惩罚Leader的投票:
首先使用本地的MoveSandbox状态验证Leader是否作弊,如果验证结果为Leader确实有作弊,投赞成票,否则投反对票。
2.3.2 MoveSandbox
MoveSandbox负责运行状态通道中的合约,生成新的状态变更,同时提供状态订阅功能。
...
代码块 |
---|
import { WasmFs } from '@wasmer/wasmfs' import { MoveSandbox } from '@starcoin/move-js' const initSandbox = async () => { const wasmfs = new WasmFs() const sandbox = new MoveSandbox(wasmfs, { storage_dir: "/workspace/storage_dir", }) const accounts = await sandbox.setModules(modules)load_accounts_from_chain(state_channel_id); await sandbox.setResourcessetAccounts(resourcesaccounts); return sandbox } initSandbox() |
...
代码块 |
---|
import { WasmFs } from '@wasmer/wasmfs' import { MoveSandbox } from '@starcoin/move-js' const executeTransation = async () => { const sandbox = initSandbox(modules, resources) let change_sets, events = await sandbox.run("0x1::Token::Token", ty_args, args) console.log(change_sets) console.log(events) } executeTransation() |
...
代码块 |
---|
import { WasmFs } from '@wasmer/wasmfs' import { MoveSandbox } from '@starcoin/move-js' const subscribeState = async () => { const sandbox = initSandbox(modules, resources) let snapshot, ch = await sandbox.subscribe() console.log(snapshot) ch.on("change_set", function(changeSet){ console.log(changeSet) }) } subscribeState() |
2.3.3 Proposals
2.3.3.1
...
结算状态通道提案
当状态通道节点检查到Leader不可达时,可以发起选主提案。超过2/3成员投票通过,选主成功。当有P2P成员希望离开状态通道时,需要发起结算状态通道提案。提案成功后方可离开状态通道,如果没有发起结算状态通道提案就离开状态通道,认为弃权,后续投票默认弃权。
提案参数:
参数 | 类型 | 描述 |
---|
leader_p2p_address
string
P2P地址地址类型
state_channel_id
string
state_channel_id | string | 状态通道ID |
from_height | u128 | 状态通道起始高度,需要和链上状态通道中的已结算高度匹配 |
to_height | u128 | 状态通道结算高度 |
change_sets | vector<u8> | 起始高度到待结算高度的所有变更集 |
2.3.3.2 惩罚作弊者提案
当状态通道收到Leader发送过来的同步消息,并在MoveVM验证结果不对时,可以发起Leader作弊的惩罚提案,如果提案执行成功,发起人为新的Leader.
提案参数:
参数
类型
描述
state_channel_id
string
参数 | 类型 | 描述 |
---|---|---|
leader_p2ptarget_address | string | P2P地址地址类型 |
state_channel_id | string | 状态通道ID |
badraw_sync_messagetransaction | string | 错误的同步消息,其中包括原始交易,验证者,验证结果 |
2.3.3.3 结算状态通道提案
当有P2P成员希望离开状态通道时,需要发起结算状态通道提案。提案成功后方可离开状态通道,如果没有发起结算状态通道提案就离开状态通道,认为弃权,后续投票默认弃权。
提案参数:
原始交易 | ||
change_sets | vector<u8> | 改原始交易对应的变更集 |
target_sign | string | 目标地址对该交易的签名 |
2.3.4 Sessions
2.3.4.1 开启会话
...
https://cookbook.starcoin.org/zh/docs/concepts/multisig/
https://wiki.biligame.com/mc/%E5%AE%9A%E5%88%B6%E6%9C%8D%E5%8A%A1%E5%99%A8 《MineCraft定制服务器》
https://www.zhihu.com/question/24459078