-
Notifications
You must be signed in to change notification settings - Fork 1
Home
asado is code for building blockchains. It is written in scala and uses AKKA IO for network communication. It attempts to isolate some parts of blockchain implementation that developers might be interested in tailoring for their own purposes.
Examining a blockchain as four requirements -
This document describes how asado meets those requirements, where the extension points are and where the code is opinionated.
Beyond the basic blockchain a proof of concept Message service has been created allowing users to charge for receiving 'mail'. This service and it's client are also described here.
FAIR WARNING This is Beta code at best, so do your own testing!
A note on potential future directions
- A ledger encapsulates state.
- Transactions are attempts to alter that state, they may succeed or fail.
- The state of a ledger may be read.
Using this fairly simple definition means many possible ledgers can be defined and supported by asado. There are two ledgers already implemented in the code base but many more can be added by implementing a simple interface ( more detail )
trait Ledger {
@throws[LedgerException]
def apply(ledgerItem: LedgerItem, blockheight: Long)
def coinbase(nodeIdentity: NodeIdentity, blockId: BlockId, ledgerId: Byte): Option[LedgerItem] = None
}
The two existing ledgers are the balance ledger and the identity ledger. The balance ledger is very similar to the bitcoin ledger and tracks the pool of TxOuts over time. TxOuts are the addresses of balances. These TxOuts are encumbered by specific contracts specified by the previous owner. The simplest of these is a demand for a signature by a private key before the output can be 'unencumbered' or spent.
All encumbrances are written in plain scala code, there is no script as per bitcoin. Encumbrances are the equivalent of 'smart contracts'.
trait Encumbrance extends Contract {
def decumber(params: Seq[Array[Byte]], context: LedgerContext, decumbrance: Decumbrance): Boolean
}
Note that given these extension points it is possible to add an encumbrance that supports a bitcoin-like script, and to create a ledger that supports an ethereum like language. Not trivial, but possible.
Existing encumbrances include 'encumber to private key', 'encumber to identity', 'sale or return', these are described in the ledgers module.
A ledger manages the state and state transitions checking the proposed transactions for validity, intuitively in a balance ledger the total amount in should equal the total amount out, and mostly this is the case. However the initial balance must come from somewhere. Given the above definitions of ledgers and contracts there are many possibilities here.
Describing a few random examples -
- A fixed amount, the genesis transaction could have a value of 10,000,000.
- The amount of money in the network could be linked to an ordinary online bank account such that the ledger controls the account and funds the identity of those making lodgements and authorises (or denies) attempted withdrawals
- A virtual currency rewarded to the signers of the latest block.
The balance ledger currently inflates the ledger by creating tokens on every block close. This is for test purposes. (See Future Plans )
The encumbrance 'encumber to private key' listed above differs from 'encumber to identity'. An output encumbered to a private key means proof of knowing the specific private key must be provided. An identity is a broader concept. An identity is a short potentially memorable string. All claimed identities are recorded in the ledger, in order to claim an identity you provide the string you wish to claim e.g. 'bob' and a public key. If bob has not been claimed, the identity bob is created and associated with the given public key. This transaction is recorded in the ledger and broadcast to the rest of the network where it forms part of the blockchain. Should someone else try to claim 'bob' their claim will fail as bob belongs to you.
To add a second backup key to bob's identity a transaction is crafted linking bob's identity with a second public key. Note that this transaction is only accepted because it is signed with the private key of the original public key bob used to claim his identity. Thus bob may assign several public keys to his identity. Going back to our encumbrance, should the private key of an an output encumbered to a private key be lost the output is gone forever, however the balance ledger output encumbered to an identity can be de-cumbered by any of the private keys in the identity ledger.
If the identity has already been claimed, the transaction is rejected, if not, that public key (and tag) is registered to identity bob. This transaction is recorded in the ledger and broadcast to the rest of the network where it forms part of the blockchain. Bob may then add other keys to his identity by sending a new transaction linking his identity with a new key. Assuming the transaction is valid it is broadcast across the network and then Bob may sign new transactions with the second key and these will also be accepted. He may continue to use his first key or he may decide to unlink his first key.
This identity bootstrapping is quite a flexible way to provide a spectrum of security. The identity may have a single key associated with it and may live for a single block or may be a persons name, have several keys and be linked to another identity trusted to sign a new key into existence should they lose all their keys.
And all these decisions are recorded on the blockchain as signed transactions all the way back to the initial claim.
The Identity Ledger ...
class IdentityLedger(ledgerId: Byte, idLedgerStorage: IdentityService) extends Ledger with Logging {
override def apply(ledgerItem: LedgerItem, blockHeight: Long): Unit = {
require(ledgerItem.ledgerId == ledgerId, s"The ledger id for this (Identity) ledger is $ledgerId but " +
s"the ledgerItem passed has an id of ${ledgerItem.ledgerId}")
val ste = ledgerItem.txEntryBytes.toSignedTxEntry
ste.txEntryBytes.toIdentityLedgerMessage match {
case Claim(identity, pKey) => idLedgerStorage.claim(identity, pKey)
case a @ UnLink(identity, tag) =>
verifyChangeRequest(ste, a, identity)
idLedgerStorage.unlink(identity, tag)
case a @ UnLinkByKey(identity, pKey) =>
verifyChangeRequest(ste, a, identity)
idLedgerStorage.unlink(identity, pKey)
case a @ Link(identity, pKey, tag) =>
verifyChangeRequest(ste, a, identity)
idLedgerStorage.link(identity, pKey, tag)
case a @ Rescue(rescuer, identity, pKey, tag) =>
verifyRescueRequest(rescuer, ste, a, identity)
idLedgerStorage.link(identity, pKey, tag)
case a @ LinkRescuer(rescuer, identity) =>
verifyChangeRequest(ste, a, identity)
idLedgerStorage.linkRescuer(identity, rescuer)
case a @ UnLinkRescuer(rescuer, identity) =>
verifyChangeRequest(ste, a, identity)
idLedgerStorage.unLinkRescuer(identity, rescuer)
}
}
def verifyRescueRequest(rescuer: String, ste: SignedTxEntry, msg: IdentityLedgerMessage, identity: String) {
require(ste.signatures.nonEmpty && ste.signatures.head.size == 2, "A tag/sig pair must be provided to continue.")
val rescuers = idLedgerStorage.rescuers(identity)
require(rescuers.contains(rescuer), s"This rescuer is not authorized to rescue $identity")
val tag = new String(ste.signatures.head(0), UTF_8)
val sig = ste.signatures.head(1)
val accOpt = idLedgerStorage.accountOpt(rescuer, tag)
require(accOpt.isDefined, s"Could not find an account for identity/tag pair ${identity}/$tag provided in signature.")
require(accOpt.get.verify(sig, msg.txId), "The signature does not match the txId")
}
def verifyChangeRequest(ste: SignedTxEntry, msg: IdentityLedgerMessage, identity: String) {
require(ste.signatures.nonEmpty && ste.signatures.head.size == 2, "A tag/sig pair must be provided to continue.")
val tag = new String(ste.signatures.head(0), UTF_8)
val sig = ste.signatures.head(1)
val accOpt = idLedgerStorage.accountOpt(identity, tag)
require(accOpt.isDefined, s"Could not find an account for identity/tag pair ${identity}/$tag provided in signature.")
require(accOpt.get.verify(sig, msg.txId), "The signature does not match the txId")
}
}
The identity ledger contains the state of all known identities and maintains the relationship between identities and their keys. Note the balance ledger depends on the identity ledger through the 'encumber to identity' encumbrance. Further on it will be shown that network access is also dependent on the identity ledger. asado is opinionated about identity. Note that an identity can represent many things, a network node, an organization, a person, a group, an event, or nothing at all....
Every transaction accepted by a ledger becomes part of a block. When a transaction arrives at the lead node in the network and is accepted by the ledger asado persists that tx to the block and propagates that tx to all connected nodes in the quorum. asado can be configured to close a block after a certain amount of time has expired or as a result of a threshold number of transactions being accepted.
When the block is closed
- the transaction id's (TxId) of all transactions are created as a hash of the transaction
- a merkle tree of all the transactions ids in the block is created and persisted.
- a block header is created from the hash of the previous block header, the root of the merkle tree and the block height
Thus every block header in the chain is derived from every previous block header which in turn is derived from every transaction in the block.
Note that only the merkle trees and block headers are required to prove the existence of a transaction in the block chain.
An asado network consists of two types of node, core nodes support the continuity of the network and a majority of these must be available to start the network. Non core nodes may drop off without affecting the network continuity. How the list of core nodes is formed varies but the simplest case is as a list of identities, ip addresses and port numbers.
peers = ["bob:127.0.0.9:7071","eve:127.0.0.9:7081"]
An asado node starting up goes through a simple state machine -
Connecting -> Quorum -> Ordered -> Ready
It begins in the 'Connecting' state and attempts to connect to all it's peers. It remains in this state until it successfully connects to over half the nodes in it's peer list. It then enters the 'Quorum' state and attempts to discover the leader node. It broadcasts it's credentials for leadership to the network. They are the latest block and the index of the latest transaction it knows about.
Our new node may be the node that allows the network to start accepting transactions eg number 3 of 5 or number 5 of 9. In this case every node is broadcasting it's latest closed block number and number of transactions in the latest unclosed block. Each node will receive the others credentials, compare them with their own, and if they out rank their own they will vote for the outranking node. The winning node has the highest numbers and will receive the most votes and become leader. Any 'lying' will prevent the network from closing the next block as the lying leader will not have transactions that the valid leader will have.
If our new node is 4 of 5 and the network is already 'up' then only the current leader will respond to the new node.
The network node moves to the 'Ordered' state.
The new node then begins to download (from the leader) any blocks that it has missed since it was last connected. Once all blocks have been downloaded the new node sends the leader a 'Synched' message and is added to the leaders pool of synchronised peers.
The network node moves to the 'Ready' state.
Our new node will now accept transactions and forward them to the leader, acting as a proxy and returning the results to the client.
When the leader receives a valid transaction it forwards it to all connected synchronised peers and returns the results to the client, thus a client getting a Tx Ack (as opposed to a Tx Nack) and one or more Tx Confirms can believe the tx has been accepted by the network. Note there is no chain reorg ever. And the tx is permanently confirmed within seconds.
To recap, only the leader can accept transactions. If the leaders ledger accepts the tx it is journalled to all connected up-to-date peers. When a peer that is not the leader receives a tx it proxies it to the leader. That same peer can expect to be asked to journal the transaction by the leader after it has been accepted by the ledger.
It journals the leaders tx providing persistent back up. At block close time, each peer connected will attempt to put every transaction it has received through the ledger and close the block. If a node closing a block fails to add a transaction to the ledger, it will fail to close the block. If this happens the leader has attempted to cheat in some way and the whole network will stop closing blocks. If the cheating leader is removed the network can restart. Valid confirmed tx's will go through as they are recorded on every peer node and the new non cheating leader will include them in the next block. asado makes an assumption that the value of the network lies in it being perceived as acting honestly.
When the peer nodes close a block they calculate a hash of the block based on the transactions in the block and the previous block header. They then sign this hash and return it to the leader. The leader will validate that the signature came from the peer and that the decrypted signature is in fact the hash of all transactions in the current block and the previous block header. This record is itself distributed among all the peers. In this way the leader knows the peers have processed the block in the same way the leader has and no peer who has signed a block can deny having seen the transactions in a block.
Non core nodes download and validate blocks but do not form part of the quorum and do not vote on a leader.
In summary
- each node (including non-core nodes) can examine every transaction and verify it.
- should the leader drop off, another leader is elected within seconds and the network continues to accept transactions.
- when a peer node returns, it syncs and becomes part of the network.
Perhaps surprisingly, the identity ledger is fundamental to network operation. asado insists on a node having an identity in the ledger before it allows a connection. The identity itself can be claimed 'out of band' using a servlet or other means. The implication is that rules for participation in the network can be created by the networks designers.
The identity is also used to sign block headers at block close time such that a history of block signers is created in parallel with the block chain itself. This list is propagated to the core nodes. this feature might be useful in regulatory environments.
A more detailed description of the network connection module
A services node includes core node functionality does but also includes sundry services such as the Message service and the Claims servlet. A services node may or may not be configured to be part of the core network.
trait ServicesNode extends CoreNode with
MessageQueryHandlerActorBuilder with
ClaimServletBuilder {
}
As a proof of concept a messaging service has been implemented in the node module. The premise of the message service is that a service can charge for the storage and delivery of an encrypted message from identity 'bob' to 'alice'.
The client creates a test message and embeds in the message a secret. It then encrypts the message with the shared secret created by the public/private key identities of both parties.
(A client node connects to either a services or core node directly. This is known as it's 'home' node. From there it receives the blockchain and to there it sends transactions. All nodes contain their own wallet associated with their identity.)
The client then attaches to the message a transaction paying the home node a delivery charge and the recipient of the message a bounty. The bounty can only be retrieved by presenting the secret embedded in the message and only by the recipient. Should a certain block height be reached without the bounty being claimed, the sender can reclaim the bounty.
'Alice', also running a client with a home node pings her home node regularly for messages, these are downloaded, decrypted and a transaction created claiming the bounty...
case class EncryptedMessage(encrypted:Array[Byte], iv: InitVector) {
lazy val toBytes: Array[Byte] = (ByteArraySerializer(encrypted) ++ StringSerializer(iv.asString)).toBytes
def decrypt(receiver: NodeIdentity,
sendPublicKey: PublicKey): TextWithSecret = {
val sharedSecret: SharedSecret = receiver.createSharedSecret(sendPublicKey)
val decryptedMessage = CBCEncryption.decrypt(sharedSecret, encrypted, iv)
textWithSecret(decryptedMessage)
}
}
Claiming the bounty ...
case ClaimBounty(ledgerItem, secret) =>
val stx = ledgerItem.txEntryBytes.toSignedTxEntry
val tx = stx.txEntryBytes.toTx
val adjustedIndex = tx.outs.size - 1
val ourBounty = tx.outs(adjustedIndex)
val inIndex = TxIndex(tx.txId, adjustedIndex)
val in = TxInput(inIndex, ourBounty.amount, SaleSecretDec)
val out = TxOutput(ourBounty.amount, wallet.encumberToIdentity())
val newTx = StandardTx(Seq(in), Seq(out))
val sig = SaleSecretDec.createUnlockingSignature(newTx.txId, nodeIdentity.tag, nodeIdentity.sign, secret)
val signedTx = SignedTxEntry(newTx.toBytes, Seq(sig))
val le = LedgerItem(MessageKeys.BalanceLedger, signedTx.txId, signedTx.toBytes)
watchingBounties += signedTx.txId.asHexStr -> BountyTracker(TxIndex(signedTx.txId, 0),out)
ncRef ! SendToNodeId(NetworkMessage(MessageKeys.SignedTx, le.toBytes), homeDomain.nodeId)
The service could be expanded to implement strategies depending on who the sender is. Some senders (dating site advertisers) or just unknown identities could be charged a large amount of currency and some (family) could post for next to nothing. This kind of service makes spam very expensive while being cheap for those who receive as much as they send.
An identity has a 'domain' postfix similar to an email address - [email protected], 'bob' must be unique across the whole ledger. The domain allows a client to know which service node to post the message to. This lends itself to a certain amount of resilience in that if a homeNode (domain) gets shut down the identity can be migrated across to another domain. It also distributes the load of message storage across domains, just like email.
-
Group identities Share an identity by linking users keys to it allowing messages to be downloaded by everyone in the group
-
The "Woody Allen" consensus Maintaining a list of hard coded peers is fine for a rigid network shared among a small number of committed supporters, but for a public facing network it would be nice to have a more dynamic automatic way of forming the peer list. The "Woody Allen" consensus or "proof of turning up" involves a peer list growing and shrinking depending on the candidate node successfully signing m of n blocks consecutively. As the number of peers grow, the amount of tokens generated per block could also increase at a formulaic rate depending on the number of connected peers. Each connected node would be rewarded by the leader for it's block signature. These parameters could be tuned to target a predetermined network 'strength' and currency inflation rate.