Constitution

The constitution for a CCF service is implemented as a set of JS scripts. These scripts can be submitted at network startup in the start.constitution_files configuration entry, or updated by a governance proposal. They will be concatenated into a single entry in the public:ccf.gov.constitution table, and should export 3 named functions:

  • validate: This takes the raw body of a proposal and checks that this proposal is correctly formed. For instance it may parse the body as JSON, extract the list of proposed actions, and confirm that each action is known and has parameters matching the expected schema. This should not interact with the KV, and should operate purely on the given proposal.

  • resolve: This takes a proposal and the votes (the results of ballot scripts) which have been submitted against it, and determines whether the proposal should be accepted or rejected. In the simple case this might simply accept proposals after a majority of members have voted in favour. It could also examine member data to give each member a different role or weight, or have different thresholds for each action. This has read-only access to the KV.

  • apply: This takes a proposal which has been accepted by resolve, and should make the proposed changes to the service’s state. For instance if the proposal added a new user this should extract their cert and data from the proposal and write them to the appropriate KV tables. This has full read-write access to the KV.

Sample constitutions are available in the samples/constitutions/ folder and include the default implementation of apply which parses a JSON object from the proposal body, and then delegates the application of each action within the proposal to a named entry from actions.js:

export function apply(proposal, proposalId) {
  const proposed_actions = JSON.parse(proposal)["actions"];
  for (const proposed_action of proposed_actions) {
    const definition = actions.get(proposed_action.name);
    definition.apply(proposed_action.args, proposalId);
  }
}

There are also more involved examples such as veto/resolve.js. This accepts proposals when a majority of members vote in favour, but also allows any single member to veto the proposal, marking it Rejected after a single vote against:

export function resolve(proposal, proposerId, votes) {
  // Every member has the ability to veto a proposal.
  // If they vote against it, it is rejected.
  if (votes.some((v) => !v.vote)) {
    return "Rejected";
  }

  const memberVoteCount = votes.length;

  let activeMemberCount = 0;
  ccf.kv["public:ccf.gov.members.info"].forEach((v) => {
    const info = ccf.bufToJsonCompatible(v);
    if (info.status === "Active") {
      activeMemberCount++;
    }
  });

  // A majority of members can accept a proposal.
  if (memberVoteCount > Math.floor(activeMemberCount / 2)) {
    return "Accepted";
  }

  return "Open";
}

There are also examples for specific member roles such as operator/resolve.js operator_provisioner/resolve.js. Operators are allowed to add and remove nodes from the network without a majority vote, and operator provisioners are allowed to endorse members to be operators, which allows customers to control the operators in the case of disaster recovery.

function getMemberInfo(memberId) {
  return ccf.bufToJsonCompatible(
    ccf.kv["public:ccf.gov.members.info"].get(ccf.strToBuf(memberId)),
  );
}

function isRecoveryMember(memberId) {
  return (
    ccf.kv["public:ccf.gov.members.encryption_public_keys"].get(
      ccf.strToBuf(memberId),
    ) ?? false
  );
}

// Defines which of the members are operators.
function isOperator(memberId) {
  // Operators cannot be recovery members.
  if (isRecoveryMember(memberId)) {
    return false;
  }
  return getMemberInfo(memberId).member_data?.is_operator ?? false;
}

// Defines which of the members are operator provisioners.
function isOperatorProvisioner(memberId) {
  return (
    !isRecoveryMember(memberId) &&
    (getMemberInfo(memberId).member_data?.is_operator_provisioner ?? false)
  );
}

// Defines actions that can be passed with sole operator provisioner input.
function canOperatorProvisionerPass(action) {
  // Operator provisioners can add or retire operators.
  return (
    {
      set_member: () => action.args["member_data"]?.is_operator ?? false,
      remove_member: () => isOperator(action.args["member_id"]),
    }[action.name]?.() ?? false
  );
}

export function resolve(proposal, proposerId, votes) {
  const actions = JSON.parse(proposal)["actions"];

  // If the node is an operator provisioner, strictly enforce what proposals it can
  // make
  if (isOperatorProvisioner(proposer_id)) {
    return actions.every(canOperatorProvisionerPass) ? "Accepted" : "Rejected";
  }

  // Count member votes.
  const memberVoteCount = votes.filter(
    (v) =>
      v.vote && !isOperatorProvisioner(v.member_id) && !isOperator(v.member_id),
  ).length;

  // Count active members, excluding operator provisioners and operators.
  let activeMemberCount = 0;
  ccf.kv["public:ccf.gov.members.info"].forEach((value, key) => {
    const memberId = ccf.bufToStr(key);
    const info = ccf.bufToJsonCompatible(value);
    if (
      info.status === "Active" &&
      !isOperatorProvisioner(memberId) &&
      !isOperator(memberId)
    ) {
      activeMemberCount++;
    }
  });

  // A majority of members can always accept a proposal.
  if (memberVoteCount > Math.floor(activeMemberCount / 2)) {
    return "Accepted";
  }

  return "Open";
}