Tutorial
In this tutorial, we explain how to build a decentralized chat application using Bitcoin Computer. We will start with a simple one-person chat and progressively expand it into a chat platform where users can create communities and even trade them trustlessly via smart contracts.
A Smart Contract
The JavaScript program below is a smart contract for a one-person chat.
import { Contract } from '@bitcoin-computer/lib'
// Define a Chat contract that extends the base Contract class
class Chat extends Contract {
/**
* Constructor to create an on-chain chat
* @param {string} greeting - The first message in the chat.
*/
constructor(greeting) {
// Initialize messages array with the first message
super({ messages: [greeting] })
}
/**
* Method to post a new message to the chat.
* @param {string} message - The message to be added.
*/
post(message) {
// Append the new message to the existing array
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'
// Initialize a new computer instance with a mnemonic
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) {
// Initialize an on-chain object with messages array
super({ messages: [greeting] })
}
post(message) {
this.messages.push(message)
}
}
new Chat('hello')
Revision (_rev)
A revision refers to a particular state of an on-chain object, tied to a specific blockchain transaction output. Each update to the object creates a new revision.
Public Key Ownership (_owners)
This property defines which public keys control (can update) the on-chain object. If multiple keys are assigned, it's a 1-of-n multisig control. The default owner is the key that created the object.
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 _satoshis:
expect(chat).to.deep.equal({
// Application-specific data
// Chat messages
messages: ['hello'], )
// Protocol speciic data added by Bitcoin Computer protocol:
// Location where the object was first created
_id: '667c...2357:0',
// Location where the object is currently stored
_rev: '667c...2357:0',
// Root object reference for object hierarchies
_root: '667c...2357:0',
// Array of owner public keys that control the object
_owners: ['03...'],
// satoshis locked in this object
_satoshis: 5820n,
})
- The properties
_id,_rev,_rootare set to strings of the form<id>:<num>where<id>is the id of the transaction broadcast by thecomputer.newcall and<num>is an output number. - The
_ownersproperty is set to an array containing public keys. - The
_satoshisproperty specifies a number of satoshis (bigint).
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
// Broadcasts transaction updating messages array
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 {
// 'world' has been added to the array of messages
messages: ['hello', 'world'],
_id: '667c...2357:0',
// the location of where the object has been stored is updated
_rev: 'de43...818a:0',
_root: '667c...2357:0',
_owners: ['03...'],
_satoshis: 5820n,
}
The property _rev has been updated and now refers to the first output of tx. The properties _id, _root, _owners, _satoshis have not changed. The meaning of these special properties is as follows:
_idis the output in which the on-chain object was first created. As this output never changes the_idproperty never changes_revis 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._rootis never updated. As it is not relevant to the chat example we refer the interested reader to this section._ownersis set to the public key of the data owner. More on thatbelow ._satoshisis 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 _satoshis 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
// Get initial state using on-chain object id
const initialChat = await computer.sync(chat._id)
// Original state preserved
expect(initialChat.messages).deep.eq(['hello'])
If computer.sync is called with latest revision it returns the current state.
// Get current state using latest revision id
const latestChat = await computer.sync(chat._rev)
// Includes all updates
expect(latestChat.messages).deep.eq(['hello', 'world'])
// Verify full object consistency
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({
// Search by original on-chain object id
ids: [chat._id],
})
// Returns most recent revision
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],
// Only readers can decrypt the object
_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.
Encryption (_readers)
Setting the _readers property on an on-chain object limits who can decrypt and read its state. Only public keys listed here can access the object’s content.
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 is the URL where the metadata is stored (including the image)
this._url = 'https://my.bitcoin.computer.node.example.com'
this.messages.push(image)
}
}
Off-Chain Storage
A feature allowing large or sensitive data to be stored outside the blockchain, with just a hash and URL on-chain pointing to the actual content. This is useful for images or large metadata.
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 satoshis in the output that stores the object. We therefore say that the satoshis are stored in the on-chain object.
The number of satoshis in the output of an on-chain object can be configured by setting the _satoshis property to a number (bigint). If this property is undefined, the object will store an a minimal (non-dust) amount.
Minimizing on-chain costs: In addition to the satoshis stored in objects, creating/updating objects typically involves minimal-dust metadata outputs (the technical cost of direct on-chain storage via bare multisig + the UTXO hygiene service). To minimize these costs (and thus user fees), prefer moduleStorageType: 'taproot' (default where available; no hygiene dust for the module tx itself), deploy larger logic once as a module so expressions stay tiny, and/or use _url (pointer only on-chain; cost becomes small constant). Direct bare-multisig is useful for single-tx reliability in high-throughput cases (e.g. exchanges) but still benefits from minimization. Full guidance, scope, bare-multisig UX rationale (incl. why not OP_RETURN), and verification notes: see the Fees "User Choices to Control On-Chain Data and Hygiene Dust Costs" section. Also review the Legal Notice.
If the value of the _satoshis 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 _satoshis 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(_satoshis, recipient) {
super({ _satoshis, _owners: [recipient] })
}
cashOut() {
this._satoshis = 546n // minimal non-dust amount
}
}
const computerAlice = new Computer({
// Alice's BIP39 recovery phrase
mnemonic: mnemonicAlice,
})
const payment = computerA.new(Payment, [21000n, 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.
// Get latest version of payment object from blockchain
const computerB = new Computer({ mnemonic: <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
envthat 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')` // JS expression to evaluate
const { tx, effect } = await computer.encode({ exp }) // Returns unsigned transaction and predicted effect
// effect shows expected outcome if transaction is broadcast
expect(effect).deep.eq({
res: {
// Resulting object state
messages: ['hello'],
_id: '667c...2357:0',
_rev: '667c...2357:0',
_root: '667c...2357:0',
_owners: ['03...'],
_satoshis: 5820n,
}
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 a JavaScript 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 {}'
// Returns on-chain module id
const specifierA = await computer.deploy(moduleA)
// Create dependent module referencing first module
const moduleB = `
// Import from on-chain module
import { A } from '${specifierA}'
export class B extends A {}
`
// Deploy second module
const specifierB = await computer.deploy(moduleB)
// Use deployed module to create contract instance
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,
// Bitcoin Script spending condition
_owners: 'OP_3 OP_EQUAL',
// 1 BTC value lock
_satoshis: BigInt(1e8),
})
}
inc() {
this.n += 1
// Transfer 0.001 BTC
this._satoshis = 100000000n - 100000n
}
}
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({
exp: 'a.inc()',
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
Recall that an on-chain object is an object that is associated with the output of a transaction that is built according to the Bitcoin Computer protocol. We will call such an object an off-chain object if the transaction has not been broadcasted yet.
It is sometimes necessary to update off-chain objects. For example, if smart contracts are used in payment channels or networks. Or, if an object is sold to an unknown buyer: in this case the object that the buyer will use is not known when seller build the transaction (see here).
Mocking is a technique for updating off-chain objects. The mocks property of the encode function can be set to a mapping from variable names to mocked objects. Here, a mocked object is an object that has _id, _rev and _root set to strings mock-${hex}:${num} where hex is a string of 64 hexadecimal characters and num is a number. In addition a revision needs to be specified for the mocked variable name in the env array (if the revision in env does not match the revision of the mocked object for the same variable name the revision in env takes precedence). When the transaction is built, it is assumed that the value stored at the revision specified in env evaluates to the value in the mocks for the same variable name.
The Bitcoin Computer library exports a class Mock that makes it easy to create mocked objects. The example below shows how a mocked object can be used. Note that in the example, the object a is created after the transaction that spends it. Thus the revision of a is not known when tx is built. Once a is created on-chain and its revision becomes known, the code below updated the input of tx to spend the revision of a.
import { Mock, Contract } from '@bitcoin-computer/lib'
class M extends Mock {
constructor() {
super({ n: 1 })
}
inc() {
this.n += 1
}
}
// Real contract
class A extends Contract {
constructor() {
super({ n: 1 })
}
inc() {
this.n += 1 // Actual implementation
}
}
// Create Mock
const m = new M()
// Create transaction that updates an object a that does not exist yet
const { tx } = await computer.encode({
// The update expression
exp: `a.inc()`,
// Map variable name a to mock m
mocks: { a: m },
// Specify that the input that spends a points to m._rev
env: { a: m._rev },
// The transaction can only be funded and signed once it is finalized
fund: false,
sign: false,
})
// Create on-chain object
const a = await computer.new(A)
// Update outpoint of tx to spend a's revision
const [txId, num] = a._rev.split(':')
const index = parseInt(num, 10)
tx.updateInput(0, { txId, index })
// Fund, sign and broadcast transaction
await computer.fund(tx)
await computer.sign(tx)
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. In order to guarantee a fast load time, each of these objects contains a constant-size chunk of messages (have a look at this section to understand the build-in optimizations). When the current chunk object is full, a new object that would refer to the previous chunk would be created. When the chat app is loaded it syncs to the the latest chunk. If the user scrolls to the first message of the chunk, the user could click a button to load the previous chunk.
In order to support an unlimited number of users, the technique sketched at the end of
If you would like to build such an app please reach out, we would be delighted to try to help you.