2.0.1 • Public • Published

    Open Publish State Engine

    This Open Publish state engine reads and validates an ordered list of Open Publish operations from the public access Bitcoin blockchain to compute a state of ownership.

    Installation and Use

    npm install openpublish-state-engine

    Like all Bitcoin metaprotocols, the Open Publish state engine needs access to a Bitcoin blockchain and a place to store validated metadata.

    var openpublishStateEngine = require('./')({
      commonBlockchain: commonBlockchain,
      openpublishOperationsStore: openpublishOperationsStore

    Open Publish adheres to the Common Blockchain interface and will work with any valid adapter, including rpc-common-blockchain and the useful for testing mem-common-blockchain. There is limited support for 3rd parties like Blocktrail or Blockcypher as most blockchain API providers do not have have access to the full block data.

    It is recommended to have bitcoind running locally to the state engine and to use rpc-common-blockchain in production.

    If you can't run bitcoind in production, Blockai runs a public-access Open Publish web service. Please see openpublish-state for more information.

    Open Publish Operations Store

    The Open Publish state engine need a place to store and query valid registration and transfer operations as well as a place to store valid tips.

    You can see a full in-memory implementation and how it is used in this project's test suite.

    Production versions should implement their own data store using a more permanent solution such as LevelDB or Postgres.

    var openpublishOperationStore = {
      pushOp: function (openpublishOperation, callback) {
        callback(false, exists)
      pushTip: function (tip, callback) {
        callback(false, exists)
      pushDividendPayment: function (payment, callback) {
      findTips: function (options, callback) {
        callback(false, matchingTips)
      findDividendPayments: function (options, callback) {
      findTransfers: function (options, callback) {
        callback(false, matchingOperations)
      findRegistration: function (options, callback) {
        callback(false, registration)
      latest: function (callback) {
        callback(false, latestOperation)
      invalidateBlock: function (blockId, callback) {
        callback(false, didInvalidate)


    Add the operation to the stack of valid Open Publish operations.

    There should be unique constraint on openpublishOperation.txid.

    This should only be called on valid operations as determined by openpublishStateEngine.validateOpenpublishOperation().


    Add the tip to the stack of valid Open Tips.

    There should be a unique constraint on tip.txid.

    This should only be called on valid tips as determined by openpublishStateEngine.validateOpenTip().


    Add the dividend payment to the stack of valid Open Tips dividend payments.

    There should be a unique constraint on payment.txid.

    This should only be called on valid tips as determined by openpublishStateEngine.validateOpenTipDividendPayment().


    Given an options.sha1, an options.destinationAddress or an options.sourceAddress, should return all matching valid Open Tips.


    Given an options.sha1, an options.destinationAddress or an options.sourceAddress, should return all matching valid Open Tip dividend payments.


    Given an options.sha1 or an options.assetAddress, should return all matching valid Open Publish transfer operations.


    Given an options.sha1, should find the single valid Open Publish registration.


    Should return the latest valid Open Publish operation.


    Should remove all operations, tips and dividend payments for the given blockId.

    Running The Open Publish State Engine

    The state engine needs to sync to a Bitcoin blockchain. It does this by reading every transaction in every block and validating every operation.

      blockHeight: 0,
      onBlock: function (err, blockInfo) {},
      onTx: function(err, tx) {},
      onOperation: function (err, validOpenpublishOperations, blockInfo) {},
      onTip: function (err, tip) {}
    }, function (err, status) {

    There are callbacks during the scanning a syncronization process for both the raw blocks and raw transactoins with onBlock and onTx respectively.

    Additionally, after every block where valid Open Publish operations were found, the onOperation function is called and for every tip, onTip.

    After every block has been parsed by the state engine there is an additional ending callback.

    It is possible to start the scan from an arbitrary options.blockHeight or options.blockId and up to a certain options.toBlockHeight.

    Validating Open Publish Operations

    Since anyone can write whatever they want to the Bitcoin blockchain, we need a mechanism that follows a set of rules in order to enforce the validity of claims, as technically valid Open Publish registrations and transfers need to be compared to the existing valid operations.

    openpublishStateEngine.validateOpenpublishOperation(operation, tx, function(err, valid) {

    There are a set of simple conditions for valid registration and transfer operations.


    As per most code related to registering ownership, "between two conflicting transfers, the one executed first prevails if it is recorded".

    openpublishOperationsStore.findRegistration({sha1: newRegistration.sha1}, function (err, existingRegistration) {
      // only the first registration is valid
      var valid = !existingRegistration


    And of course valid transfers are contingent on the balances of the accounts involved.

    getAssetBalance({sha1: newTransfer.sha1, assetAddress: newTransfer.assetAddress}, function (err, assetBalance) {
      var valid = assetBalance > newTransfer.assetValue

    Computing Asset Balance

    Given a document's options.sha1 and a Bitcoin wallet options.assetAddress, we compute current assetBalance.

    openpublishStateEngine.getAssetBalance({sha1: sha1, assetAddress:wallet.address}, function (err, assetBalance) {

    Balances are computed by a sum of all related transactions for the asset and account in question.

    openpublishOperationsStore.findTransfers({sha1: options.sha1}, function (err, existingValidTransfers) {
      openpublishOperationsStore.findRegistration(options, function (err, existingRegistration) {
        var assetBalance = 0
        if (existingRegistration && existingRegistration.addr === options.assetAddress) {
          assetBalance += ONE_HUNDRED_MILLION
        existingValidTransfers.forEach(function (transfer) {
          if (transfer.bitcoinAddress === options.assetAddress) {
            assetBalance += transfer.assetValue
          if (transfer.assetAddress === options.assetAddress) {
            assetBalance -= transfer.assetValue
        callback(false, assetBalance)

    Computing Capitalization Table

    We can also compute the full capTable for a given options.sha1.

    openpublishStateEngine.getCapitalizationTable({sha1: sha1}, function (err, capTable) {
      // capTable object
        msLoJikUfxbc2U5UhRSjc2svusBSqMdqxZ: 99960000,
        mwaj74EideMcpe4cjieuPFpqacmpjtKSk1: 10000,
        mjM1Zrm8JGnCF4hENLy4TdP9fEL5QWyp59: 30000 

    The cap table is computed by iterating over all valid transactions including the intial registration.

    Please note that the cap table always sums to default registration value of 100,000,000.

    Validating Open Tips

    openpublishStateEngine.validateOpenTip(tip, tx, function(err, valid) {

    Valid tips need to be directed to the original account with the matching sha1 registration.

    openpublishOperationsStore.findRegistration({sha1: sha1}, function (err, existingRegistration) {
      var valid = existingRegistration && existingRegistration.addr === tipDestinationAddress

    Computing Dividends Payable Table

    We can compute the dividends that are owed to each asset holder.

    openpublishStateEngine.getOpenTipDividendsPayableTable({sha1: sha1}, function(err, dividendsPayableTable) {
      // dividendsPayableTable object
        msLoJikUfxbc2U5UhRSjc2svusBSqMdqxZ: -5000,
        mwaj74EideMcpe4cjieuPFpqacmpjtKSk1: 5000 

    We do this by looking at each Open Tip and computing the cap table at the time the tip was mined. This means that different tips for the same sha1 could have different cap tables.

    openpublishStateEngine.getCapitalizationTable({sha1: sha1, tip: tip}, function (err, capTable) {
      // capTable object
        msLoJikUfxbc2U5UhRSjc2svusBSqMdqxZ: 50000000,
        mwaj74EideMcpe4cjieuPFpqacmpjtKSk1: 50000000

    The dividends payable to each address is the tip amount multiplied by the percent holdings of the asset.

    for (var address in capTable) {
      var percentage = capTable[address] / ONE_HUNDRED_MILLION
      var dividendPayable = parseFloat((tipAmount * percentage).toFixed(10))
      if (existingRegistration.addr === address) {
        modifyTable(address, -dividendPayable)
      } else {
        modifyTable(address, dividendPayable)

    Validating Open Tip Dividend Payments

    openpublishStateEngine.validateOpenTipDividendPayment(payment, tx, function(err, valid) {

    Valid dividend payments need to be directed to one of the addresses in the cap table while coming from the registration address.

    var validSource = existingRegistration && existingRegistration.addr === tipSourceAddress
    getCapitalizationTable({sha1: sha1}, function (err, capTable) {
      var valid = validSource && capTable[tipDestinationAddress] && capTable[tipDestinationAddress] > 0
      callback(err, valid)


    npm i openpublish-state-engine

    DownloadsWeekly Downloads






    Last publish


    • williamcotton