#
Tutorial
In this tutorial we explain how to build a decentralized chat. We will start with a comically simple version of a one person chat, but we will work our way up to a chat platform where users can build a community and sell it trustlessly through a smart contract.
#
A Smart Contract
The Javascript program below is a smart contract for a one-person chat.
import { Contract } from '@bitcoin-computer/lib'
class Chat extends Contract {
constructor(greeting) {
super({ messages: [greeting] }) // initialize messages array
}
post(message) {
this.messages.push(message)
}
}
We recommend to ignore the syntax for initializing messages
in the constructor for now. If you are interested in the details you can find them here.
#
The Client-Side Wallet
The Computer
class is a client-side Javascript wallet that manages a Bitcoin private-public key pair. It can create normal Bitcoin transactions but also ones that contain metadata according to the Bitcoin Computer protocol. This allows for the creation, updating, and retrieval of on-chain objects.
You can pass a BIP39 mnemonic into the constructor to create a specific private-public key pair, or leave the mnemonic
parameter undefined to generate a random wallet. More configuration options are described here.
import { Computer } from '@bitcoin-computer/lib'
const computer = new Computer({ mnemonic: 'replace this seed' })
#
Creating On-Chain Objects
The computer.new
function broadcasts a transaction inscribed with a Javascript expression consisting of a class and a constructor call. For example, the call
const chat = await computer.new(Chat, ['hello'])
broadcasts a transaction inscribed with the expression below.
class Chat extends Contract {
constructor(greeting) {
super({ messages: [greeting] })
}
post(message) {
this.messages.push(message)
}
}
new Chat('hello')
The Bitcoin Computer protocol interprets such a transaction as creating an on-chain object of type Chat
at the first output of the transaction.
The object chat
returned from the computer.new
call is similar to the object returned from the call new Chat('hello')
. However it has additional properties _id
, _rev
, _root
, _owners
and _amount
:
expect(chat).to.deep.equal({
messages: ['hello'],
_id: '667c...2357:0',
_rev: '667c...2357:0',
_root: '667c...2357:0',
_owners: ['03...'],
_amount: 5820,
})
- The properties
_id
,_rev
,_root
are set to strings of the form<id>:<num>
where<id>
is the id of the transaction broadcast by thecomputer.new
call and<num>
is an output number. - The
_owners
property is set to an array containing public keys. - The
_amount
property specifies an amount in satoshis.
We refer to chat
as an on-chain object.
Note that the transaction that created chat
does not contain an encoding of the object itself. It only contains code that evaluates to its state.
#
Updating On-Chain Objects
When a function is called on an on-chain object, a transaction is broadcast that is inscribed with a Javascript expression encoding the function call and an environment determining the undefined variables in the expressions. For example, the call
await chat.post('world')
broadcasts a transaction tx that is inscribed with the data below.
{
exp: 'chat.post('world')'
env: [chat]
}
The transaction's first input spends the output in which the previous revision of the chat
object was stored.
Note that it is not possible to compute a value from the expression chat.post('world')
alone because the variable chat
is not defined. To make the expression determined the transaction's inscription contains an environment env
that associates the variable name chat
with its first input.
To compute the value of the chat
after the post
function is called and the transaction tx is broadcast, the Bitcoin Computer protocol first computes the value stored at the output spent by the first input of tx. This value is then substituted for the name chat
in the expression chat.post('world')
. Now the expression is completely determined and can be evaluated. The new value for chat
is associated with the first output of tx. In our example, this value is
Chat {
messages: ['hello', 'world'],
_id: '667c...2357:0',
_rev: 'de43...818a:0',
_root: '667c...2357:0',
_owners: ['03...'],
_amount: 5820,
}
The property _rev
has been updated and now refers to the first output of tx. The properties _id
, _root
, _owners
, _amount
have not changed. The meaning of these special properties is as follows:
_id
is the output in which the on-chain object was first created. As this output never changes the_id
property never changes_rev
is the output where the current revision of the object is stored. Therefore, initially, the revision is equal to the id, then the revision is changed every time the object is updated._root
is never updated. As it is not relevant to the chat example we refer the interested reader to this section._owners
is set to the public key of the data owner. More on thatbelow .amount
is set to the amount of satoshi of the output in which an on-chain object is stored. Morehere .
The properties _id
, _rev
, and _root
are read only and an attempt to assign to them throws an error. The properties _owners
and _amount
can be assigned in a smart contract to determine the transaction that is built.
The state of the on-chain objects is never stored in the blockchain, just the Javascript expression that creates it. This makes it possible to store data compressed to its Kolmogorov complexity which is optimal.
#
Reading On-Chain Objects
The computer.sync
function computes the state of an on-chain object given its revision. For example, if the function is called with an the id of an on-chain object, it returns the initial state of the object
const initialChat = await computer.sync(chat._id)
expect(initialChat.messages).deep.eq(['hello'])
If computer.sync
is called with latest revision it returns the current state.
const latestChat = await computer.sync(chat._rev)
expect(latestChat.messages).deep.eq(['hello', 'world'])
expect(latestChat).to.deep.equal(chat)
#
Finding On-Chain Objects
The computer.query
function returns an array of strings containing the latest revisions of on-chain objects. For example, it can return the latest revision of an object given its id:
const [rev] = await computer.query({ ids: [chat._id] })
expect(rev).to.equal(chat._rev)
A basic pattern in applications is to identify a on-chain object by its id, to look up the object's latest revision using computer.query
, and then to compute its latest state using computer.sync
. For example, in our chat app the url could contain a chat's id and the latest state of the chat could be computed as shown below.
// Extract the id from the url
const id = urlToId(window.location)
// Look up the latest revision
const [rev] = await computer.query({ ids: [id] })
// Compute the latest state
const latestChat = await computer.sync(rev)
computer.query
can also return all revisions of on-chain objects owned by a public key. This could be useful for creating a user page for the chat application.
// Get public key from a client side wallet
const publicKey = computer.getPublicKey()
// Look up all revisions owned by user
const revs = await computer.query({ publicKey })
It is also possible to navigate the revision history of a on-chain object using computer.next
and computer.prev
:
// Navigating forward
expect(await computer.next(chat._id)).eq(chat._rev)
// Navigating backward
expect(await computer.prev(chat._rev)).eq(chat._id)
Note that the code above only works because there are only two revisions of the chat in our example, otherwise computer.next
or computer.prev
would have to be called multiple times.
#
Data Ownership
We are finally ready to elevate our one-person chat to a three-person chat! We will explain how to allow an unlimited number of users
The owner of an on-chain object is the user that can spend the output that stores it, just like the owner of the satoshi in a output is the user that can spend it.
The owners can be set by assigning the _owners
property. If this property is set to an array of public keys, the output script is a 1-of-n bare multisig script, meaning that any owner can update the object. If it is undefined the owner default to the public key of the computer object that created the on-chain object.
In the chat example, the initial owner is the public key of the computer
object on which computer.new
function was called. Only that user can post to the chat. We can add a function invite
to update the owners array to allow other users to post.
class Chat extends Contract {
... // like above
invite(pubKeyString) {
this._owners.push(pubKeyString)
}
}
While a user can never change an on-chain object that they do not own, the owner has complete control. This includes the ability to destroy their own objects by spending their outputs with a transaction that does not conform to the Bitcoin Computer protocol. In this case the value of the object will be an Error value.
This is reminiscent of the real world where people have the right to destroy their own property but not the right to destroy somebody else's property.
#
Encryption
By default, the state of an on-chain object is public in the sense that any user can compute its state by using computer.sync
. However, read access can be restricted by setting an objects _readers
property to an array of public keys. If _readers
is assigned, the meta-data on the transaction is encrypted using a combination of AES and ECIES so that only the specified readers have read access.
For example, to ensure that only people invited to the chat can read the messages, you can update our example code as follows:
class Chat extends Contract {
constructor(greeting, owner) {
super({
messages: [greeting],
_readers: [owner],
})
}
invite(pubKey) {
this._owners.push(pubKey)
this._readers.push(pubKey)
}
}
As all updates to an on-chain object are recorded in immutable transactions it is not possible to restrict access to a revision once it is granted. It is also not possible to grant read access to a revision without granting read access to its entire history as the entire history is needed to compute the value of a revision. It is however possible to revoke read access from some point forward or to restrict access to all revisions all together.
When on-chain objects are encrypted the flow of cryptocurrency is not obfuscated.
#
Off-Chain Storage
It is possible to store the metadata of a transaction off-chain in the database of a Bitcoin Computer Node. In this case a hash of the metadata and a url where the metadata can be retrieved is stored on chain, while the metadata itself is stored on the server. To use this feature, set a property _url
of an on-chain object to the URL of a Bitcoin Computer Node.
For example, if users want to send images to the chat that are too large to store on-chain, they can use the off-chain solution:
class Chat extends Contract {
// ... as above
post(message) {
this._url = null
this.messages.push(message)
}
postImage(image) {
this._url = 'https://my.bitcoin.computer.node.example.com'
this.messages.push(image)
}
}
#
Cryptocurrency
Recall that an on-chain object is stored in an output and that the owners of the object are the users that can spend the output. Thus the owners of an object are always the owners of the satoshi in the output that stores the object. We therefore say that the satoshi are stored in the on-chain object.
The amount of satoshi in the output of an on-chain object can be configured by setting the _amount
property to a number. If this property is undefined, the object will store an a minimal (non-dust) amount.
If the value of the _amount
property is increased, the additional satoshi must be provided by the wallet of the computer
object that executes the call. In the case of a constructor call with computer.new
that is that computer
object. In the case of a function call it is the computer
object that created the on-chain object.
If the value of the _amount
property is decreased, the difference in satoshi is credited to the associated computer object's wallet.
For example, if a user Alice wants to send 21000 satoshis to a user Bob, then Alice can create an on-chain object of the following Payment
class.
class Payment extends Contract {
constructor(amount, recipient) {
super({ _amount, _owners: [recipient] })
}
cashOut() {
this._amount = 546 // minimal non-dust amount
}
}
const computerAlice = new Computer({ mnemonic: mnemonicAlice })
const payment = computerA.new(Payment, [21000, pubKeyBob])
When the payment
on-chain object is created, the wallet inside the computerA
object funds the 21000 satoshi that are stored in the payment
object. Bob can withdraw the satoshi by calling the cashOut
function.
const computerB = new Computer({ seed: <B's mnemonic> })
const paymentB = await computerB.sync(payment._rev)
await paymentB.cashOut()
#
Expressions
The syntax for on-chain objects introduced above provides a high-level abstraction over the Bitcoin Computer protocol. However we also provide low-level access to the protocol via the computer.encode()
function. This gives more control over the transaction being built, enabling advanced applications like DEXes.
The computer.encode
function takes three arguments:
- A Javascript expression
exp
, - an environment
env
that maps names to output specifiers, - and a module specifier
mod
.
It returns a transaction but does not broadcast it. Therefore calling the encode
function does not alter the state of any on-chain object. In addition to the transaction the function returns an object effect
that represents the state that will emerge on-chain if the transaction is broadcast.
The effect
object returned from the encode
function provides absolute certainty about a transaction's impact before broadcasting. If the transaction is included, the state updates exactly as reflected in the effect
object, independent of other users' transactions; otherwise, the state remains unchanged.
The effect
object has two sub-objects: res
contains the value returned from the expression and env
contains the side-effect, specifically the new values of the names in the environment.
The code below is equivalent to calling await computer.new(Chat, ['hello'])
. In fact the computer.new
function is just syntactic sugar for using the encode
function.
const exp = `${Chat} new Chat('hello')`
const { tx, effect } = await computer.encode({ exp })
expect(effect).deep.eq({
res: {
messages: ['hello'],
_id: '667c...2357:0',
_rev: '667c...2357:0',
_root: '667c...2357:0',
_owners: ['03...'],
_amount: 5820,
}
env: {}
})
The encode function allows fine grained control over the transaction being built via an options object as a second argument that can specify
- whether to fund the transaction
- whether to include or exclude specific UTXOs when funding
- whether to sign the transaction
- which sighash type to use when signing
- which inputs to sign
- whether to mock objects to do not exist yet on-chain (see more
below )
#
Module System
The computer.deploy
function stores an ES6 modules on the blockchain. It returns a string representing the output where the module is stored. Modules can refer to one another using the familiar import
syntax. In the example below moduleB
refers to moduleA
via specifierA
. Module specifiers can be passed into computer.encode
and computer.new
functions.
const moduleA = 'export class A extends Contract {}'
const specifierA = await computer.deploy(moduleA)
const moduleB = `
import { A } from '${specifierA}'
export class B extends A {}
`
const specifierB = await computer.deploy(moduleB)
const { tx } = await computer.encode({
exp: `new B()`,
mod: specifierB,
})
Modules can be loaded from the blockchain using computer.load.
const loadedB = await computer.load(specifierB)
expect(loadedB).eq(moduleB)
#
Bitcoin Script Support
In addition to setting the _owners
property to an array of strings as described _owners
to a string encoding a Bitcoin Script ASM. In this case the output created for that on-chain object will use a pay-to-script-hash (p2sh) output with that script.
import { Contract } from '@bitcoin-computer/lib'
class A extends Contract {
constructor() {
super({
n: 1,
_owners: 'OP_3 OP_EQUAL',
_amount: 1e8,
})
}
inc() {
this.n += 1
this._amount = 1e8 - 100000
}
}
In order to spend an output with a p2sh script, one needs to alter the transaction returned from encode
. The example below shows how to build a transaction with an inputs script OP_3
.
import { Computer } from '@bitcoin-computer/lib'
import { payments } from '@bitcoin-computer/nakamotojs'
const computer = new Computer()
const a = await computer.new(A)
// Encode transaction that encodes update
const { tx } = await computer.encode({
// Call the 'inc' function
exp: 'a.inc()',
// Spend output at revision a._rev
env: { a: a._rev },
// transaction can only be signed and funded when it is completed
sign: false,
fund: false,
})
// The code below creates a p2sh input script `OP_3`. To create a p2sh input
// script the redeem-script of the output script being spent needs to be
// specified as well. In our case the output redeem-script is `a._owners`.
const { input } = payments.p2sh({
redeem: {
// Specify input script ASM
input: fromASM('OP_3'),
// Specify the output redeem-script
output: fromASM(a._owners),
},
})
// Add input script to transaction
tx.setInputScript(0, input!)
// Broadcast transaction
await computer.broadcast(tx)
A Bitcoin Script could be used to allow more than three users to post to a chat. The idea is to use a Taproot script that has one spending path for each user. Taproot scripts have a large number of spending paths, so a large number of users could be supported. When this kind of Taproot chat is updated, only the spending path for the posting user needs to be revealed, meaning that the cost to post is constant and independent of the number of users in the chat.
#
Mocking
It is sometimes necessary to build a Bitcoin Computer transaction that spends an output of a transaction that has not been broadcast to the blockchain. One example is using smart contracts over payment channels or networks. Another example is selling an object to an unknown buyer: in this case the object to be used for the payment is not known when the seller builds the transaction (see for example here).
To facilitate such application the Bitcoin Computer has a feature called Mocking. "Mocked" objects can be passed into the encode
function. In this case, when a transaction is build the Bitcoin Computer protocol assumes that an object with that value exists at the specified revision. To create a mock you can just instantiate an object that extends from Mock
.
import { Mock, Contract } from '@bitcoin-computer/lib'
class M extends Mock {
constructor() {
super()
this.n = 1
}
inc() {
this.n += 1
}
}
class A extends Contract {
constructor() {
super({ n: 1 })
}
inc() {
this.n += 1
}
}
To use a mock, pass it into the mocks
property of the encode
function. When the object being mocked up becomes available on the blockchain, the mocked transaction can be modified to point to the actual object.
// Create Mock
m = new M()
// Create mocked transaction that updates an object a that
// does not exist yet.
const { tx } = await computer.encode({
// exp contains the update expression
exp: `a.inc()`,
// mocks determines the value assumed to exist at outpoint
mocks: { a: m },
// env determines the outpoint to spend, as no object a exists yet
// we set it to the revision of the
env: { a: m._rev },
// the transaction cannot be funded and signed until it is finalized
fund: false,
sign: false,
})
// Create on-chain object
a = await computer.new(A)
const [txId, index] = a._rev.split(':')
// Update outpoint of mocked transaction to point to on-chain transaction
tx.updateInput(0, { txId, index: parseInt(index, 10) })
// Fund, sign and broadcast transaction
await computer.fund(tx)
await computer.sign(tx)
const txId2 = await computer.broadcast(tx)
#
Building the Chat Platform
We now sketch how a chat platform could be built where moderators can create a community and then sell it over the internet.
Every community would be represented through multiple on-chain objects that are similar to the Chat
object. Each of these objects contains a constant-size chunk of messages. When the current chunk object is full, a new object that would refer to the previous chunk would be created. When a user joins a chat the app would sync to the latest chunk. If the user scrolls up to the first message of the chunk, the user could click a button to load the previous chunk of messages.
In order to support an unlimited number of users, the technique sketched at the end of
Many more details need to be figured out (for example how to implement a "moderator object" and how to prevent abuse). If you want to build such an app please reach out, we would be delighted to try to help you.