@hyperdivision/heimdall

Heimdall is a secure application server for collaborative, auditable decisions

Usage no npm install needed!

<script type="module">
  import hyperdivisionHeimdall from 'https://cdn.skypack.dev/@hyperdivision/heimdall';
</script>

README

heimdall

Heimdall is a secure application server for collaborative, auditable decisions

Usage

const heimdall = require('heimdall')

const server = heimdall.server('./server')

server.operation('withdraw-btc', {
  valueEncoding: 'json',
  execute (proposal, state, done) {
    console.log('gonna withdraw btc:', proposal.data)
    done(null)
  }
})

server.ready(function (err) {
  if (err) throw err

  const client = heimdall.client('./client', server.key)

  const rs = server.replicate({live: true})
  rs.pipe(client.replicate({live: true})).pipe(rs)

  client.propose('withdraw-btc', 10)
})

Install

npm install heimdall

API

Key generation

const { publicKey, secretKey } = heimdall.keygen.hypercore([secretKey])

Create a new hypercore key pair (main key in Heimdall). If secretKey is not provided, a new key pair will be created. Otherwise the public key will be derived from the secret key.

Server API

server = heimdall.server(storage, [options])

Create a new heimdall server instance. Storage should be a random-access-instance instance or a folder name where you want the server data to be stored.

Options include:

{
  genesisKeys: [keys...], // or
  genesisUsers: [{ key, profile, profileSignature }, ...]
}

Genesis keys are the initial clients that should be trusted to vote and make proposals.

server.operation(name, operation)

Add a new operation.

Operation should look like this:

{
  inputEncoding: <optional-input-decoder>,
  outputEncoding: <optional-output-decoder>,
  valueEncoding: <optional input/output encoding, 'json', 'utf8' etc>,
  execute: function (proposal, state, done) {
    // use state to access per operation state
    // execute your operation here.
    // call done with (err, data) when done
  },
  reducer: function (proposal, state, done) {
    // called when operation output is being processed.
    // you can access the output data as proposal.output
    // you can populate state with any kind of state you need in your
    // operation
    // call done with (err) when done
  },
  quorum: function (proposal, state, done) {
    // check proposal.votes against proposal.eligibleVoters
    // to see if there is a quorum
    // should call done(err, heimdall.quorum.PASS,REJECT,UNKNOWN)
    // when done
  },
  validate: function (proposal, done) {
    // validate that the proposal is in fact properly formed
    // ie, that the user provided data looks sane
    // should call done(err, isValid) when done
  }
}

If you want to create a catch all operation you can use the symbol, server.OPERATION_WILDCARD as the name. This requires setting the allowWildcard to true in the server constructor.

Some stock quorums ship with heimdall and can be accessed using heimdall.quorum. They include:

{
  majority, // simple >50% pass
  superMajority, // >66% pass
  percentage(pct), // if pct is >0 then >50% yays pass, if <0 then >50% nays reject
  absolute(cnt), // same as percentage using an absolute count of yays, nays
  tally(proposal) // helper that returns {yays, nays, abstain, undecided, total} from a proposal
}

server.on('trust-key', key)

Emitted when the server trusts a new public key.

server.on('revoke-key', key)

Emitted when the server revokes a new public key.

server.on('proposal', proposal, seq)

Emitted when a new proposal is accepted.

A proposal looks like this:

{
  op: 'operation-name',
  data: <input data decoded using the operation input decoder>,
  link: <strong link to this proposal>,
  votes: <array of votes cast on this proposal>,
  eligibleVoters: <array of keys that can vote on this proposal>,
  links: { proposal, relay, pass, reject }
}

A vote looks like this:

{
  key: <key of voter>,
  weight: -1, 0, 1 // <0 is a nay, 0 is abstain and >0 is a yay
}

In addition to the lifecycle events for the proposals emitted on the server and client objects the proposal above will emit the following as well

  • active - when the proposal is active, meaning it's been accepted and relayed by the server peer.
  • passed - when the proposal has passed the quorum.
  • rejected - when the proposal has been rejected according to the quorum.
  • executed - when the proposal has passed and been executed.
  • expired - when the proposal has expired.
  • vote - when a vote has been cast on the proposal.

server.on('vote', vote, proposal, seq)

Emitted when a new vote is accepted for a proposal.

server.on('pass', proposal, seq)

Emitted when a proposal passes.

server.on('reject', proposal, seq)

Emitted when a proposal is rejected.

server.on('expire', proposal, seq)

Emitted when a proposal has expired.

Either pass, reject, or expire is guaranteed to fire for any proposal.

server.on('execute', proposal)

Emitted when the server starts executing a proposal.

server.on('executed', proposal)

Emitted when the server is done executing a proposal.

server.key

The server's public key

Client API

client = heimdall.client(storage, [serverKey], [options])

Make a new client instance.

options include options.gc which is a boolean for removing proposals from the proposal store after they become inactive. gc is enabled by default.

Use options.reindex if you want to reindex the local set if you migrate any of your operations.

client.propose(operation, data, [callback])

Propose a new operation to run.

var bool = client.isEligible(proposal)

Propose a new operation to run.

var bool = client.hasVoted(proposal)

Check if client have already voted for this proposal.

var bool = client.canVote(proposal)

Check whether client can vote for proposal, ie. have they already voted, are they eligible, is the proposal in an active state etc. Does NOT check whether the client is trusted.

var bool = client.hasPendingVote(proposal)

Check if client has a pending vote on proposal. A vote is pending until it has been relayed by the server.

var vote = client.getOwnVote(proposal)

Get the vote by client on proposal. First tries to find an relayed vote and then checks pending votes. Returns null if no vote exists. Note that the vote object returned is not reference stable to whichever vote may eventually become relayed.

client.vote(link, weight, [data], [callback])

Vote on a proposal. weight should be either -1, 0, 1 depending on whether you vote nay, abstain, or yay.

For simplicity the weights are exported on the heimdall module as

  • heimdall.VOTE_APPROVE
  • heimdall.VOTE_ABSTAIN
  • heimdall.VOTE_REJECT

If you wish to pass a binary payload to the vote, ie a signature or similar to be used when a quorum exists, you can pass this using the data argument.

client.data(type, value, [link], [callback])

Append a new data message. Will be relayed through the server immediately. Useful for data synchronisation.

Callback is called when it's appended to the local feed. If you pass in a link to a proposal the data will be appended to proposal.additionalData as well.

client.on('data', type, value, link, feed, seq, relay)

Emitted when a data message is received.

client.on('sync')

Emitted when the client is fully in sync with the latest server version it has seen.

The client.synced property can used to check if the feed is currently synced or not.

client.on('trust-key', key)

Emitted when the server trusts a new public key.

client.on('revoke-key', key)

Emitted when the server revokes a new public key.

client.on('status-change')

Emitted when the server trusts or revokes the clients public key.

client.on('trusted')

Emitted when the server trusts the clients public key.

client.on('revoked')

Emitted when the server revokes the clients public key.

client.on('proposal', proposal, seq)

Emitted when a new proposal is accepted.

A proposal looks like this:

{
  op: 'operation-name',
  data: <input data decoded using the operation input decoder>,
  link: <strong link to this proposal>,
  votes: <array of votes cast on this proposal>,
  eligibleVoters: <array of keys that can vote on this proposal>,
  links: { proposal, relay, pass, reject },
  timestamps: { proposal, relay, pass, reject }
}

A vote looks like this:

{
  key: <key of voter>,
  weight: -1, 0, 1, // <0 is a nay, 0 is abstain and >0 is a yay
  data: <optional buffer> // Vote payload optionally provided by the user
}

client.on('vote', vote, proposal, seq)

Emitted when a vote for a proposal has been accepted.

client.on('pass', proposal, seq)

Emitted when a proposal passes.

client.on('reject', proposal, seq)

Emitted when a proposal is rejected.

client.on('expire', proposal, seq)

Emitted when a proposal has expired.

Either pass, reject, or expire is guaranteed to fire for any proposal.

client.on('executed', proposal, seq)

Emitted when a proposal is done executing.

License

ISC