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.