Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

group property support #71

Open
streetycat opened this issue Dec 1, 2022 · 8 comments
Open

group property support #71

streetycat opened this issue Dec 1, 2022 · 8 comments
Assignees
Labels
CYFS Stack This is CYFS Stack feature New feature

Comments

@streetycat
Copy link
Collaborator

We need multiple users to collectively hold and maintain the same information, which is consistent among different members and can be verified.

@streetycat streetycat added the feature New feature label Dec 1, 2022
@lurenpluto lurenpluto added the CYFS Stack This is CYFS Stack label Dec 1, 2022
@streetycat
Copy link
Collaborator Author

streetycat commented Dec 1, 2022

  1. Abandon the current SimpleGroup standard object, and provide a group object object that can contain multiple People objects. The group objects can support dynamic configuration related attributes (members, power, etc.)
  2. Provide the GroupState structure, save the R-Path status information of the Group's group on the OOD of the members of the Group, the RootState design similar
/--${groupid}
    |--${decid}
        |--field1-->Obj1
        |--field2-->Obj2
  1. The consensus agreement is currently using BFT (HOTSTUFF?)
  2. POST/GET initiated to Group can automatically address its members and submit it
  3. For information obtained from Group, Group provides method verification (mainly the process of verifying the signature)
  4. Support CYFS: // R/$ {Groupid}/$ {deciD}/$ {R-Path}
  5. Provide access permissions control of GroupState ACL
  6. Provide the permissions of the operation of Group-ACL

@streetycat
Copy link
Collaborator Author

streetycat commented Jan 3, 2023

How to use it for developers?

For the client, it can commit a proposal, and view the status of the DEC:

https://github.com/streetycat/CYFS/tree/main/src/component/cyfs-group/src/dec/rpath_client.rs

The administrator is responsible for maintaining DEC status with BFT consensus. For APP developers, four interfaces are required:

  1. Collect proposals, including authentication, and read the group settings to provide a default implementation; The startup process is included in this process. After authentication, the status is synchronized

  2. For the implementation of proposals, when it is local's turn to be the outgoing node, the collected proposals should be implemented

  3. Validate the implementation results of the proposal. When the local node is used as the verification node, verify the implementation results of the outgoing node; Considering the possibility of malicious administrators, it is necessary for APP to participate in the verification, but it can provide a default verification implementation (fully trust the versions of all administrators)

  4. For fork selection, the group may change due to application needs, and the nodes may be inconsistent. In extreme cases, APP is required to help you decide which fork to choose.

There is the interface that developers need to implement:
https://github.com/streetycat/CYFS/tree/main/src/component/cyfs-group/src/dec/delegate_factory.rs

There is the example of the initial release
https://github.com/streetycat/CYFS/tree/main/src/component/cyfs-group/examples/app_example.rs

I'm sorry for the uncompilable pseudo code.

@streetycat
Copy link
Collaborator Author

How to handle a proposal

There are two important roles: member and administrator. The administrator is a group of members who have the power and responsibility to maintain the consistency of group status. Members (including administrators) drive group status changes by proposing to the administrator; The administrator synchronizes the group status to the local, and finally drives the application status display

The specific flow chart is as follows

https://miro.com/app/board/uXjVP0eNwhM=/

@streetycat
Copy link
Collaborator Author

streetycat commented Jan 10, 2023

How to view the state for group

Group administrators maintain the consistency of their management state through BFT consensus. This state may be a tree aggregation state, called the root state. Only the root state can be verified directly, and its sub-state needs to be verified indirectly through the root state

Any user with read permission can read the root status value and signature on the corresponding rpath path from the group member, including the sub-path status value and other values necessary for verification. After reading, the user can verify, and finally the APP can calculate the final status as needed.

To which member to read the status, you can set a priority for exploratory polling.

The flow chart is as follows:
https://miro.com/app/board/uXjVPzjGNLs=/

@streetycat
Copy link
Collaborator Author

streetycat commented Jan 10, 2023

How to verify the state

Considering that some members of the group may be malicious, the obtained status needs to be verified.

The verification of the root state can be achieved only by verifying the signature rate of the corresponding block.

The verification of sub-states is more troublesome. It is necessary to obtain all relevant sub-states on its relative path, and finally verify the root state obtained after the operation of these sub-states.

For example, if you want to read the status of '/a/b/c', you need to obtain all sub-statuses directly under ['/a', '/a/b'], and finally find the value with key 'c' in all sub-statuses of '/a/b'

There is the possibility of massive data sets, which will be a huge amount of computation and need further consideration.

@streetycat
Copy link
Collaborator Author

streetycat commented Jan 11, 2023

Draft for storage structure

Storage for group-state:

|--${group-id} // one group
|   |--${DecId("shells", ${group-id})}
|   |   |--.shells
|   |       |--.latest-->GroupShell // latest version
|   |       |--${group.version}-->GroupShell // add shells for history versions of group
|   |--${dec-id}  // one dec for a group
|       |--${r-path}
|           |--.dec-state-->ObjectId // for dec;the latest state of all groups
|           |   // one state of a r-path, It's calculated by the app, and it's a map-id in most times
|           |   // Each state change for same ${r-path} is serial
|           |   // Each state change for different ${r-path} is parallel
|           |   // **The process of state change for different ${r-path} should always not be nested, Because the change of each branch will affect the state of the root**
|           |--.link // Blockchain for hotstuff, record the state change chain,Most of the time, application developers do not need to pay attention
|               |--group-blob-->BLOB(Group) // the latest group, it's store as chunk, so, it'll not be updated by different version
|               |--users // info of any user, is useful?
|               |   |--${user-id}
|               |       |--xxx
|               |--last-vote-round-->u64 // the round that I voted last time
|               |--last-qc-->GroupQuorumCertificate
|               |
|               |--range-->(${first_height}, ${header_height}) // the range retained, we can remove some history
|               |--str(${height})->block // commited blocks with any height, QC(Quorum Certificate) by next blocks at least 2
|               |
|               |--prepares // prepare blocks, with QC for pre-block(pre-commit/commited), but not QC by any one
|               |   |--${block.id}
|               |         |--block
|               |         |--result-state-->ObjectId(result-state) // hold the ref to avoid recycle
|               |--pre-commits // pre-commit blocks, with QC for the header block, and is QC by a prepare block
|               |   |--${block.id}
|               |         |--block
|               |         |--result-state-->ObjectId(result-state) // hold the ref to avoid recycle
|               |
|               |--finish-proposals // The proposal is de-duplicated. Proposals that exceed the timeout period are directly discarded, and those within the timeout period are de-duplicated by the list
|               |   |--flip-time-->Timestamp // the timestamp of the first block
|               |   |--recycle-->Set<ObjectId>
|               |   |--adding-->Set<ObjectId>

And the cache storage in the DecAPP

|--${root} // config by the DecAPP
    |--${group-id}
        |--${r-path}
            |--state-->ObjectId // the latest state, will sync from `/.dec-state/${group-id}/${dec-id} /${r-path}/`
            |--header-block-->Block // the header block
            |--qc-->qc-block // the qc for the ${block}

The reader will read the state with the r-path(/${group-id}/${dec-id}/${r-path}/${sub-path}); and the member will return the values with above. The form is slightly awkward. Do you have any optimization suggestions? And It may be a good idea to design a built-in virtual mapping structure on the RootState of APP.

@streetycat
Copy link
Collaborator Author

Show the definition of Group

#[derive(Clone, Debug, RawEncode, RawDecode)]
pub enum GroupDescContent {
    SimpleGroup(SimpleGroupDescContent),
    Org(OrgDescContent),
}

impl GroupDescContent {
    pub fn founder_id(&self) -> &Option<ObjectId> {
        match self {
            GroupDescContent::SimpleGroup(desc) => &desc.founder_id,
            GroupDescContent::Org(desc) => &desc.founder_id,
        }
    }
}


#[derive(Clone, Debug, RawEncode, RawDecode)]
pub enum GroupBodyContent {
    SimpleGroup(SimpleGroupBodyContent),
    Org(OrgBodyContent),
}

impl DescContent for GroupDescContent {
    fn obj_type() -> u16 {
        ObjectTypeCode::Group.into()
    }

    type OwnerType = SubDescNone;
    type AreaType = Option<Area>;
    type AuthorType = SubDescNone;
    type PublicKeyType = SubDescNone;
}

impl BodyContent for GroupBodyContent {
    fn format(&self) -> u8 {
        OBJECT_CONTENT_CODEC_FORMAT_RAW
    }
}

#[derive(Clone, Debug)]
pub struct GroupMember {
    pub id: ObjectId,
    pub title: String,
}

#[derive(Clone, Debug, Default)]
struct CommonGroupBodyContent {
    name: Option<String>,
    icon: Option<String>,
    description: Option<String>,

    members: HashMap<ObjectId, GroupMember>,

    ood_list: Vec<DeviceId>,

    version: u64,
    prev_shell_id: Option<ObjectId>,
}

#[derive(Clone, Debug)]
pub struct SimpleGroupDescContent {
    unique_id: UniqueId,
    founder_id: Option<ObjectId>,
    admins: HashMap<ObjectId, GroupMember>,
}

#[derive(Clone, Debug, Default)]
pub struct SimpleGroupBodyContent {
    common: CommonGroupBodyContent,
}

#[derive(Clone, Debug)]
pub struct OrgDescContent {
    unique_id: UniqueId,
    founder_id: Option<ObjectId>,
}

#[derive(Clone, Debug, Default)]
pub struct OrgBodyContent {
    admins: HashMap<ObjectId, GroupMember>,
    common: CommonGroupBodyContent,
}

@streetycat
Copy link
Collaborator Author

Maintain organization information

Form an organization

To set up an organization in the CYFS system is to construct a Group object and publish it to the MetaChain, the method is no different from the construction and chaining of other objects. However, there are several requirements for the chaining of Group objects:

  1. There must be full signatures of all members (Desc and Body);
  2. The initiator can be empty, otherwise it must be one of the administrators;
  3. The ObjectShellId of the previous Group version must be empty;

In addition to using the same methods as other objects for construction, signature, and chaining in the program, support has also been added to the command line tool desc-tool.

  • Construct Group object

    For specific usage, directly enter desc-tool create group -h to view the help information as follows:

    >desc-tool create group -h
    
    USAGE:
        desc-tool.exe create group [FLAGS] [OPTIONS] --admins <admins> --area <area>
    
    FLAGS:
        -h, --help Prints help information
        -O, --org create a group as organization that administrators are changeable.
        -V, --version Prints version information
    
    OPTIONS:
        -A, --admins <admins> admins in group. format [PeopleId:title]
        -a, --area <area> Object area info. format [county:carrier:city:inner]
        -d, --description <description> description of group
        -F, --founder <founder> founder of group
        -I, --icon <icon> icon of group
            --idfile <id_file> write object id to file
        -M, --members <members> members in group. format [PeopleId:title]
        -n, --name <name> name of group
        -l, --oodlist <ood_list> oods in group
            --savepath <save_path> save file path
    

    For example, if we want to construct a Group containing 4 administrators, 2 ordinary members, and 4 OODs, all without title, we can use the following command:

    .\desc-tool.exe create group -F=5r4MYfFfTakY1h6vdEuMurpkawk4MZpB5RmY9CFqSj99 -A=5r4MYfFfTakY1h6vdEuMurpkawk4MZpB5RmY9CFqSj99:;5r4MYfFPPRDNNcJdvve4XV -M=5r4MYfF5r9cUfL -l=5aSixgN8tVt1SAM4xBfc1dYvdrU7d5fVeZrzNFpx8FiB;5aSixgMxgNu -n ="group" -I="icon" -d="description" -a=0:0:0:0 -O --savepath="./" --idfile="./group.id.txt"
    

    After the above command is executed, a .desc file named after the newly constructed GroupId will appear in the current working directory (specified by the savepath parameter), which is the encoded data of the newly constructed Group.

  • Distribute the constructed Group encoded data to each member in sequence, asking them to sign for confirmation:

.\desc-tool sign ${desc-path} -s=${signer-secret-path} -t=${signer-desc-path} -dba
  • Publish to MetaChain
>cyfs-meta-client.exe putdesc -h
create or update desc on meta, exclude userdata

USAGE:
     cyfs-meta-client.exe putdesc [FLAGS] [OPTIONS] --caller <caller> [ARGS]

FLAGS:
     -h, --help Prints help information
     -u force update body time on put
     -V, --version Prints version information

OPTIONS:
     -c, --caller <caller> desc and sec file path, exclude extension [default: C:\Users\Bucky\.cyfs\owner]
     -d, --desc <desc> desc file send to meta, default caller`s desc
     -m, --meta_target <meta_target> meta client target [default: dev]
     -v, --value <value> balance from caller to desc account when create [default: 0]

ARGS:
     <price> desc rent
     <coin_id> coin id

Update organization information

The method of updating the organization is the same as other mutable objects, updating Body information, and then publishing to MetaChain, but updating Group has the following requirements:

  1. All new members should sign the new Group;
  2. The new Group must hold the full signatures of more than half of the administrators in the latest version of Group on the chain;
  3. The new Group should clearly mark that it is only upgraded from the latest version on the current MetaChain, that is, its prev_shell_id field points to the ObjectShellId of the latest Group;
  4. The version number of the new Group must be increased by 1 based on the version number of the latest Group on the current chain.

Similarly, we can use the desc-tool tool to complete the update operation:

>desc-tool modify -h

modify desc

USAGE:
     desc-tool modify [FLAGS] [OPTIONS] <desc>

FLAGS:
         --appstart start app, default false
     -h, --help Prints help information

OPTIONS:
         --add_admin <add_admins> append administrators to group. format [PeopleId:title]
         --add_member <add_members> append members to group. format [PeopleId:title]
     -o, --add_ood <add_oods> device id append to people or group
     -A, --admins <admins> set administrators to group. format [PeopleId:title]
         --appid <app_id> app id add to app list
         --appver <app_ver> app ver add to app list
     -d, --description <description> description of group
     -e, --eps <eps> new endpoint list
     -I, --icon <icon> icon of people or group
     -m, --members <members> set members to group. format [PeopleId:title]
     -n, --name <name> name of people or group
     -l, --ood_lists <ood_lists> device id set to people or group
         --prev_shell <prev_shell_id> prev-shell-id of group
         --rm_admin <remove_admins> remove administrators from group. format [PeopleId]
         --rm_member <remove_members> remove members from group. format [PeopleId]
     -s, --sn <sn> new sn list
         --source <source> add source to app, {ver}:{id}
     -v, --version <version> version of group

ARGS:
     <desc> desc file to modify

Examples are as follows:

.\desc-tool modify 67fz8e9wt6Yqb9ex61tr2C1PwCqavBLFe153wfiVAhqQ.desc --add_admin=5r4MYfFBsQqy4r2LTccK1yyipRTtAjqvhX3GLU2qX3Lo --add_member=5r4MYfFapPzrXhfxWJNZJ f4pk5Ncfrx5ax2yumWKZrZj
.\desc-tool modify 67fz8e9wt6Yqb9ex61tr2C1PwCqavBLFe153wfiVAhqQ.desc --add_ood=5aSixgNLsF6r3qjDKP3XkBwnDRSr5G5hrGRM2v2LnLoA
.\desc-tool modify 67fz8e9wt6Yqb9ex61tr2C1PwCqavBLFe153wfiVAhqQ.desc --prev_shell=9cfBkPt2RPa3MofMmsAXpq8xHYn8A2xvVPuQiBT4XTp9 -v=2

The process of collecting signatures and uploading to the chain is the same as creating a Group and will not be repeated here.

Save different versions of Group objects in NON

During the above update process, Group objects are stored in the file system. In DecApp, the update process of Group should be automated, so it is necessary to store different versions of Group objects in the NON system of each member, at least two versions of Group should be stored needs:

  1. The latest version Group is the same as the latest version on the chain;
  2. The Group which is being updated is collecting signatures from each member, and has not yet been released on the chain.

The ObjectShell core object is provided in NON of CYFS, and a shell is added to different versions of the object. The shell objects of different versions are all brand new objects. Their ObjectId is different, and they can coexist in the NON system .

For the statement about ObjectShell, see issue 219

Maintain shared data

We use the Hotstuff algorithm to allow Group members to jointly maintain shared data. This section describes how to implement it in DecApp.

Role

  1. Ordinary users can only operate through Group member agents, and their behavior is completely defined by DecApp itself;
  2. Group members, who can initiate proposals;
  3. Group OOD, usually composed of Group administrators OOD, run server programs based on Hotstuff to maintain consistency on these OOD, they are responsible for collecting proposals initiated by Group members, and execute.

Initiate a proposal

Initiating proposals is the process of Group members.

  1. Open the protocol stack
let stack = SharedCyfsStack::open_default(dec_id).await?;
stack.wait_online(None).await?;
  1. Open GroupManager
let group_mgr = GroupManager::open_as_client(
         stack,
     ).await?;
  1. Initiate a proposal

The method of initiating a proposal is the same as the cross-Zone request, using the post_object method of NON, and setting the target parameter to the corresponding GroupId. The following is an example code:

     // post proposal to the admins, it's same as calling to non. post_object with default parameters;
     // and you can call the non. post_object with more parameters.
     async fn post_proposal(
         &self,
         proposal: &GroupProposal,
     ) -> BuckyResult<Option<NONObjectInfo>> {
         let req_path = RequestGlobalStatePath::new(
             Some(proposal. rpath(). dec_id(). clone()),
             Some("group/proposal"),
         );
         self. stack
             .non_service()
             .post_object(NONPostObjectOutputRequest {
                 common: NONOutputRequestCommon {
                     req_path: Some(req_path. format_string()),
                     source: None,
                     dec_id: self.0.local_dec_id.clone(),
                     level: NONAPILevel::Router,
                     target: Some(proposal.rpath().group_id().clone()),
                     flags: 0,
                 },
                 object: NONObjectInfo::new(proposal.desc().object_id(), proposal.to_vec()?, None),
             })
             .await
             .map(|resp| resp.object)
     }

The above process is also customized in the Group module:

let client = group_mgr.rpath_client(group_id, dec_id, rpath).await;

let result_object = client.post_proposal(proposal).await; // The above sample code is the implementation code of `post_proposal`

Start the service

Services are processes that run on Group OOD.

  1. Open the protocol stack
let stack = SharedCyfsStack::open_default(dec_id).await?;
stack.wait_online(None).await?;
  1. Open GroupManager
let group_mgr = GroupManager::open(
         stack,
         rpath_delegate_factory // DelegateFactory, construct and process `Delegate` corresponding to `rpath` request, handle(fn () -> Delegate)
     ).await?;
  1. Start the service RPathService
     let service = group_mgr
         .start_rpath_service(
             group_id,
             rpath.to_string(),
             Box::new(MyRPathDelegate::new(
                 local_name.to_string(),
                 cyfs_stack. clone(),
                 dec_app_id.object_id().clone(),
             )),
         )
         .await
  1. Listen to the GroupProposal object
  • Refer to the permission system, just ensure that you can receive GroupProposal objects from Group members;
  • Mount the Handle that listens to the GroupProposal object.
     //demo

     stack
         .root_state_meta_stub(None, None)
         .add_access(GlobalStatePathAccessItem::new(
             "group/proposal",
             AccessString::full().value(),
         ))
         .await;

     stack
         .router_handlers()
         .post_object()
         .add_handler(
             RouterHandlerChain::Handler,
             format!("group-proposal-listener-{}", dec_app_id).as_str(),
             0,
             Some(filter),
             Some(req_path),
             RouterHandlerAction::Pass,
             Some(Box::new(routine)),
         )
         .await;
  1. Post GroupProposal to RPathService
let result = service.push_proposal(&proposal).await;
  1. Implement the aforementioned DelegateFactory
     #[async_trait::async_trait]
     impl DelegateFactory for GroupRPathDelegateFactory {
         async fn create_rpath_delegate(
             &self,
             group_id: &ObjectId,
             rpath: &str,
             with_block: Option<&GroupConsensusBlock>,
             is_new: bool,
         ) -> BuckyResult<Box<dyn RPathDelegate>> {
             if self.is_accept(group_id, rpath, with_block) {
                 // If accepted, provide the processing response object for the rpath
                 // define your delegate
                 Ok(Box::new(RPathDelegate))
             } else {
                 Err(BuckyError::new(BuckyErrorCode::Reject, ""))
             }
         }
     }
  1. Implement RPathDelegate
     #[async_trait::async_trait]
     impl RPathDelegate for MyRPathDelegate {
         async fn on_execute(
             &self,
             proposal: &GroupProposal,
             prev_state_id: &Option<cyfs_base::ObjectId>,
             object_map_processor: &dyn GroupObjectMapProcessor,
         ) -> BuckyResult<ExecuteResult> {
             // Execute `proposal` based on the current state `prev_state_id` and return the new state
         }

         async fn on_verify(
             &self,
             proposal: &GroupProposal,
             prev_state_id: &Option<cyfs_base::ObjectId>,
             execute_result: &ExecuteResult,
             object_map_processor: &dyn GroupObjectMapProcessor,
         ) -> BuckyResult<()> {
             // Check the correctness of the execution result of `proposal`, return Ok if it is correct, and return Err if it is wrong
         }

         async fn on_commited(
             &self,
             prev_state_id: &Option<cyfs_base::ObjectId>,
             block: &GroupConsensusBlock,
             object_map_processor: &dyn GroupObjectMapProcessor,
         ) {
             // The proposal has been submitted to the consensus chain and has been approved by `2f+1` `OOD` signatures, this is its final execution result
         }
     }

Query the result

TODO: Any proposal execution will be synchronized to the RootState corresponding to DecApp of each member. We can access them through the interface of RootState, but the synchronization function of ObjectMap has not been completed yet, so we cannot provide support for the time being. The target query method is as follows:

     let query_state_stub = client_stack.root_state_accessor_stub(
         Some(group. desc(). object_id()),
         Some(*dec_app_id. object_id()),
     );

     let resp = query_state_stub
         .get_object_by_path(path)
         .await
         .unwrap();

But for the time being, the final calculation result can only be obtained from the aforementioned RPathDelegate::on_commited.

Example

group-example implements the function of a Group to jointly maintain an integer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CYFS Stack This is CYFS Stack feature New feature
Projects
Status: 🚧In Progress
Development

When branches are created from issues, their pull requests are automatically linked.

2 participants