We want to offer two functionnalities:
- Stateless zkapps: lock some funds and whoever can create a proof can spend all the funds.
- Stateful zkapps: initialize some authenticated state on-chain and update it by providing a proof.
Let's see how both system works:
A stateless zkapp can be deployed by anyone (e.g. Alice) with a transaction to 0xzkBitcoin
that contains only one data field:
- Then digest of a verifier key.
In more detail, the transaction should look like this:
Transaction {
version: Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![/* alice's funding */],
output: vec![
// one of the outputs is the stateless zkapp
TxOut {
value: /* amount locked */,
script_pubkey: /* p2tr script to zkBitcoin pubkey */,
},
// the first OP_RETURN output is the vk hash
TxOut {
value: /* dust value */,
script_pubkey: /* OP_RETURN of VK hash */,
},
// any other outputs...
],
}
In order to spend such a transaction, someone (e.g. Bob) needs to produce:
- The verifier key that hashes to that digest.
- An unsigned transaction that consumes a stateless zkapp (as input), and produces a fee to the zkBitcoin fund (as output). All other inputs and outputs are free.
- A proof that verifies with a single public input: a truncated transaction (so that the proof authenticates that specific transaction).
To reiterate, the public input is structured as follows:
PI = [truncated_tixd]
When observing such a valid request, the MPC committee will sign the zkapp input and return it to Bob.
In more detail, the following transaction is produced by Bob and sent to the MPC committee:
Transaction {
version: Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![
// one of the inputs contains the stateless zkapp
TxIn {
previous_output: OutPoint {
txid: /* the zkapp txid */,
vout: /* the output id of the zkapp */,
},
script_sig: /* p2tr script to zkBitcoin */,
sequence: Sequence::MAX,
witness: Witness::new(),
}
// any other inputs...
],
output: vec![
// one of the outputs contains a fee to zkBitcoinFund
TxOut {
value: /* ZKBITCOIN_FEE */,
script_pubkey: /* locked for zkBitcoinFund */,
}
// any other outputs...
],
}
A statefull zkapp can be deployed with a transaction to 0xzkBitcoin
that contains two data field:
- The digest of a verifier key.
- 1 field element that represent the initial state of the zkapp. (If there's none the zkapp is treated as a stateless zkapp.)
Note: we are limited to 1 field element as Bitcoin nodes don't forward transactions with more than one
OP_RETURN
output. AnOP_RETURN
seems to be limited to pushing 80 bytes of data, as such we are quite limited here.
In more detail, the transaction should look like this:
Transaction {
version: Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![/* alice's funding */],
output: vec![
// one of the outputs contain the stateful zkapp
TxOut {
value: /* amount locked */,
script_pubkey: /* p2tr script to zkBitcoin */,
},
// an OP_RETURN output containing the vk hash concatenated with the state
TxOut {
value: /* dust value */,
script_pubkey: /* OP_RETURN of VK hash and new state */,
},
// arbitrary spendable outputs are also allowed...
],
}
In order to spend such a transaction Bob needs to produce:
- The verifier key that hashes to that digest.
- An unsigned transaction that consumes a stateful zkapp (as input), and produces a fee to the zkBitcoin fund as well as a new stateful zkapp (as outputs). All other inputs and outputs are free.
- A number of public inputs in this order:
- The previous state as 1 field element.
- The new state as 1 field element.
- A truncated SHA-256 hash of the transaction id (authenticating the transaction).
- An amount
amount_out
to withdraw. - An amount
amount_in
to deposit.
- A proof that verifies for the verifier key and the previous public inputs.
To reiterate, the public input is structured as follows:
PI = [new_state | prev_state | truncated_txid | amount_out | amount_in ]
Note: we place
new_state
first, because outputs in Circom are placed first (see this tweet).
Because Bob's transaction will contain the new state, Bob needs to run a proof with truncated_txid=0
first in order to obtain the new state, then run it again with the txid
obtained. For this reason, it is important that the output of the circuit is not impacted by the value of truncated_tixd
.
When receiving such a valid request (e.g. proof verifies), the MPC committee signs the zkapp input of the transaction and returns it to Bob.
In more detail:
Transaction {
version: Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![
// one of the inputs contains the stateful zkapp
TxIn {
previous_output: OutPoint {
txid: /* the zkapp txid */,
vout: /* the output id of the zkapp */,
},
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
}
// other inputs are allowed...
],
output: vec![
// one of the outputs is a fee to the zkBitcoin fund
TxOut {
value: /* ZKBITCOIN_FEE */,
script_pubkey: /* locked for zkBitcoinFund */,
}
// one of the outputs contain the new stateful zkapp
TxOut {
value: /* the zkapp value updated to reflect amount_out and amount_in */,
script_pubkey: /* locked for zkBitcoin */,
},
// an OP_RETURN output containing the vk hash as well as the new state
TxOut {
value: /* dust value */,
script_pubkey: /* OP_RETURN of VK hash and new state */,
},
// arbitrary spendable outputs are also allowed...
],
}