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.
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.
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
.
The state engine needs to sync to a Bitcoin blockchain. It does this by reading every transaction in every block and validating every operation.
openpublishStateEngine.scanFrom({
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
.
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
})
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)
})
})
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.
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
})
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)
}
}
Followed by accounting for all existing dividend payments.
openpublishOperationsStore.findDividendPayments({sha1: sha1}, function (err, payments) {
payments.forEach(function (payment) {
var address = payment.tipDestinationAddresses[0]
modifyTable(address, -payment.tipAmount)
modifyTable(existingRegistration.addr, payment.tipAmount)
})
callback(err, dividendsPayableTable)
})
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)
})