Skip to content

Multi-node Dapp Membership

Applications on STRATO must initially be deployed to the blockchain network before users can actually use and interact with it. This deployment process typically involves gathering start-up variables like service-user authorization tokens, organization and and node info, etc. and then using that information to deploy a specific instance of smart contracts to the chain. After this, the application "infrastructure" is on the blockchain and you can now make function calls and make more contracts that operate within your app.

However, because a Dapp is a Distributed Application on a Blockchain Network, multiple machines need to be able to interact with the same instance of the deployed app on the network and run the application's backend/frontend. So how do you ensure that every server is talking to the same contracts? Typically, we save the necessary identifying information of that deployed application to a deploy file. This is a JSON or YAML (or similar) file that holds data like contract addresses and shard IDs for the main application entry point, and any other information strictly required for a new machine to use the application.

Because many applications live in Private Dapp Realms, new nodes cannot request access to a Private Dapp Realm directly in that realm, they can only be invited/added manually, therefore applications can follow either one of the following flows:

Pre-registered Nodes/Members

This method works best when membership is constrained to a known set of parties. It does not allow for “app discovery”.

This method assumes that you already know all of the information about what STRATO nodes/organizations should be a part of this application. Their membership info info must be captured in a file and read in during deployment time. These members are then added to the Private Dapp Realm when it is created. Then the secondary nodes can run their server with the deploy file and it will instantly be able to communicate with the Private Dapp Realm. Note that running the server with the deploy file does instigate the STRATO node syncing the Private Shard data, rather it simply provides the application the necessary info to access the shard on the network. Otherwise there would be no way for a secondary machine to know the ID of the proper Dapp Realm.

Nodes that must be added after deployment time must be added through some type of “invite” system - either in a UI or CLI tool. In most applications, this is done through a CLI tool where the new Node’s info is inputted by the user and it is submitted so that node is added to the Private Dapp Realm. A CLI is the easiest method since typically adding members has to be done by an admin user anyways and a UI is an extra piece of software that must be developed.

This is the way the Asset Framework currently handles application Private Realm membership.

Example

Boot Members YAML:

members:
    - orgName: Acme
      orgUnit: ''
      commonName: ''

Application Code:

import { fsUtil, importer } from 'blockapps-rest'
async function createChain(user, defaultOptions) {
    const contractSrc = await importer.combine("my/contract/path/Dapp.sol")

    //query the certificate database to get the current users org info
    const myCert = await certificateJs.getCertificateMe(user)

    //read the boot members from disk
    const membersList = fsUtil.getYamlFile("my/deploy/path/boot_members.yaml").members | []

    const chainArgs = {
        name: contractName,
        src: contractSrc,
        label: "MyApp-Shard",
        args: {...},
        members: [
            ...membersList,
            {
                orgName: myCert.organization,
                orgUnit: myCert.organizationalUnit || '',
                commonName: '',
            },
        ],
        balances: [],
        metadata: {
            VM: 'SolidVM',
        },
    }

    const contractArgs = { name: contractName }

    const chain = await rest.createChain(user, chainArgs, contractArgs, defaultOptions)

}

Public Request

This method works best when you want new nodes to be able to be a part of the app on the fly, however it does require that some information about app members be on the main chain.

The application deployment process must also include a separate contract on the main chain that exposes an interface for new nodes to request access to your application. Then the address of this contract should be made publicly available so that users may uses it in the future. The contract can also contain the deployment information itself since nodes would not be able to use them until they are a part of that Private Dapp Realm.

Then your application must also expose a way to read the requests from that contract and approve and add members from that list, or deny them and do nothing.

Example

Dapp Request Contract (posted on the Main Chain):

contract MembershipRequest {
    string public org;
    string public orgUnit;
    string public commonName;
    string public note;
    constructor(
        string _org,
        string _orgUnit,
        string _commonName,
        string _note
    ) {
        org = _org;
        orgUnit = _orgUnit;
        commonName = _commonName;
        note = _note;
    }
}

contract MyDappMembershipManager {
    // allows users to view the name and description of your dapp
    string public dappName;
    string public dappDescription;

    address[] public requests;
    // a mapping between contract address and their indices into the requests array
    mapping(address => uint) public requestsMap;
    address public contractOwner;

    constructor(string _dappName, string _dappDescription) {
        contractOwner = tx.origin;
        dappName = _dappName;
        dappDescription = _dappDescription;
    }

    // function accepts the blockchain address of a node with an identity
    function requestMembershipAddress(address _memberAddress, string _note) {
        mapping(string => string) memberCert = getUserCert(_memberAddress);
        require(memberCert["organization"] != "");
        MembershipRequest request = new MembershipRequest(
            memberCert["organization"],
            memberCert["organizationalUnit"],
            memberCert["commonName"],
            _note
        );
        address temp = address(request);
        requestsMap[requests.length] = temp
        requests.push(temp);
    }

    // function accepts an identity directly
    function requestMembershipIdentity(
        string _org,
        string _orgUnit,
        string _commonName,
        string _note
    ) {
        require(_org != "");
        MembershipRequest request = new MembershipRequest(
            _org,
            _orgUnit,
            _commonName,
            _note
        );
        address temp = address(request);
        requestsMap[requests.length] = temp
        requests.push(temp);
    }

    function removeRequest(address _requestAddress) {
        require(tx.origin == contractOwner);
        uint i = requestsMap[_requestAddress];
        requests[i] = address(0);
        requestsMap[_requestAddress] = -1;
    }


}

Private Dapp Contract:

contract MyDapp {

    address public membershipManager;
    address public contractOwner;

    // shard membership events
    event OrgAdded(string orgName);
    event OrgUnitAdded(string orgName, string orgUnit);
    event CommonNameAdded(string orgName, string orgUnit, string commonName); 

    event OrgRemoved(string orgName);
    event OrgUnitRemoved(string orgName, string orgUnit);
    event CommonNameRemoved(string orgName, string orgUnit, string commonName);

    constructor(address _membershipManager) {
        membershipManager = _membershipManager;
        contractOwner = tx.origin;
    }

    function acceptMembership(address _membershipRequest) {
        MyDappMembershipManager manager = MyDappMembershipManager(account(membershipManager, "main"));
        uint i = manager.requestsMap()[_membershipRequest];
        require(i >= 0);

        address requestAddress = manager.requests()[i];
        require(requestAddress != address(0));
        MembershipRequest request = MembershipRequest(account(requestAddress, "main"));

        if (request.orgUnit() == "") {
            emit OrgAdded(request.org());
            return;
        }
        if (request.commonName == "") {
            emit OrgUnitAdded(request.org(), request.orgUnit());
            return;
        }
        emit CommonNameAdded(request.org(), request.orgUnit(), request.commonName());
            return;
    }
}

While it is redudant to get the request's contract address from the MembershipManager if you already have the address passed in to the acceptMembership function, however it is demonstrated here how a Private Dapp contract might interact with a request manager contract on the main chain.