0443 - Draft - Standards - May 8, 2025 (1 year, 2 months ago)
8
2025
BIP: 443
Layer: Consensus (soft fork)
Title: OP_CHECKCONTRACTVERIFY
Author: Salvatore Ingala <[email protected]>
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP 443
Status: Draft
Type: Standards Track
Created: 2025-05-08
License: BSD-3-Clause
Post-History: 2022-11-08: https://gnusha.org/pi/bitcoindev/CAMhCMoH9uZPeAE_2tWH6rf0RndqV+ypjbNzazpFwFnLUpPsZ7g@mail.gmail.com
2023-07-30: https://gnusha.org/pi/bitcoindev/CAMhCMoFYF+9NL1sqKfn=ma3C_mfQv7mj2fqbqO5WX[email protected]
This BIP proposes to add consensus support for a new tapscript opcode
that enables a new type of output restrictions:
OP_CHECKCONTRACTVERIFY
(
OP_CCV
).
This opcode enables users to create UTXOs that carry a dynamic commitment to a piece of data. The commitment can be validated during the execution of the script, allowing introspection to the committed data. Moreover, a script can constrain the internal public key and taptree of one or more outputs, and possibly the committed data.
In conjunction with an opcode for vector commitments 1 , this allows to create and compose arbitrary state machines that define the possible future outcomes of a UTXO. The validity of a state transition depends on the conditions that can be expressed in the program (scripts in the taptree).
This document is licensed under the 3-clause BSD license.
The ability to constrain the future of coins beyond what is possible with presigned transactions is at the core of numerous attempts to improve bitcoin. Some of the proposed applications include:
OP_VAULT
2
is a proposed opcode to implement a
2-step withdrawal process, enabling on-chain reactive security.
OP_CHECKTEMPLATEVERIFY
3
is
a long-proposed opcode to constrain a transaction to a
template
with a fixed set of outputs.
Constructions like BitVM 4 try to side-step the lack of a primitive allowing UTXOs to carry state with a clever use of Lamport Signatures, and optimistic execution of smart contracts. This comes with an extremely high cost in term of complexity, interactivity, and (potentially) in block size occupation, for some of the possible execution paths. Moreover, the design of fully trustless bridges remains elusive.
Rather than providing a construct optimized for a specific application, this BIP aims to provide a fundamental building block that is widely applicable, and common to many constructions.
OP_CHECKCONTRACTVERIFY
is an implementation of a new
primitive that could be called
state-carrying UTXOs
. It allows
to embed a commitment to a piece of data in a UTXO, and to validate it
during the execution of the script, and
carry
a (possibly
dynamically computed) piece of data to the new UTXOs that are
produced.
We consider the program of a P2TR UTXO to be composed of an x-only public key (that we call the naked key ), and a Merkle tree of scripts. If there is no data committed in the UTXO, then the naked key is the internal key as defined in BIP 341.
If the UTXO carries a commitment to a 32-byte hash (the data ), the naked key is tweaked with a hash of the data. The resulting key is the taproot internal key per BIP 341.
This allows to embed a commitment to the data that can be validated during the script execution, while staying fully compatible with taproot. Notably:
cheapest way of spending the UTXO — albeit not always possible in practice.
;
OP_CHECKCONTRACTVERIFY
can be applied to introspect the
program and data of one of the inputs of the transaction (typically, the
UTXO being spent, in order to access its committed data), or one of the
outputs of the transaction (in order to define its program, and possibly
its committed data).
When checking the script of one or more outputs with
OP_CHECKCONTRACTVERIFY
, it is usually necessary to also
check that the amount of the current input (that is, the UTXO being
spent) is correctly distributed among the outputs in the expected way.
Therefore, the opcode already includes an amount semantic that covers
the common use cases.
There are three supported modes for the opcode when checking an
output, depending on the value of the
mode
parameter:
The default logic covers the common case where a UTXO’s full amount is required to be sent to a specific output.
The deduct logic allows to assign portions of the input amount to one or more outputs; the residual amount, checked with a final check using the default logic, can be used to enforce that the total amount is preserved.
The following figures illustrate some common examples of supported use cases for the amount logic. This list is not exhaustive, as there are many more possible combinations.
Remark: validation fails if the amount of an output is checked with both the default and the deduct logic in the same transaction, or multiple times with the deduct logic. This prevents duplicate or inconsistent counting of the same amounts.
Remark: it is allowed for multiple inputs to check the same output with the default logic. This allows multiple inputs to aggregate (in full or in part) their amounts to the same output.
A
sends the entire amount
to some output contract
B
, using
CCV
with the
default
semantic.
CCV
with the
default
semantic.
A
sends a portion of its
amount to a contract
A'
identical to itself, and the rest
to a different contract
B
. It would use
CCV
to
introspect its own input's program, then to check the first output with
the
deduct
semantic, then to check the second output with the
default
semantic to assign the residual amount.
B
also checks the same output
X
with the
default
semantic, aggregating its input with the residual amount
of the first input.
Note that the deduct semantic does not allow to check the exact amount of its output. Therefore, in contracts using a scheme similar to figure 3 or 4 above, amounts should be constrained either with a signature, or with future introspection opcodes that allow fixing the amount. In lack of that, amounts would be malleable.
Since the amount-checking semantics of
OP_CHECKCONTRACTVERIFY
are designed to preserve the entire
input amount across one or more outputs, transaction fees must be paid
exogenously. This can be achieved by adding an extra input to the
transaction, by using an anchor output, or with other future
mechanisms.
The
ignore
amount mode is not a safe mechanism for paying
endogenous fees. An output checked with this mode has no amount
constraint, which would allow a miner to claim the entire value of that
input. This mode is included for forward compatibility with potential
future soft forks that may introduce other amount-related logic that is
compatible with
OP_CHECKCONTRACTVERIFY
's script checks.
The tapscript opcode
OP_SUCCESS187
(
0xbb
)
is constrained with new rules to implement
OP_CHECKCONTRACTVERIFY
.
When evaluating
OP_CHECKCONTRACTVERIFY
(
OP_SUCCESS187
,
0xbb
), the expected format of
the stack, shown bottom-to-top, is:
<data> <index> <pk> <taptree> <mode>
where:
<mode>
is a minimally encoded integer, according
to one of the values defined below.
<taptree>
is the Merkle root of the taproot tree,
or a minimally encoded
-1
, or the empty buffer.
<pk>
is called the
naked key
, and it's a
valid 32-byte x-only public key, or a minimally encoded
-1
,
or the empty buffer.
<index>
is a minimally encoded -1, or a minimally
encoded non-negative integer.
<data>
is a buffer of arbitrary length.
In short, the semantics of the opcode with respect to the script can be summarized as follows:
Verify that the input/output with the given 'index' is a P2TR UTXO whose taproot output key is obtained from 'pk', tweaked with the hash of 'data' (if non-empty), then taptweaked with 'taptree' (if non-empty).
If the
<data>
is non-empty, then the additive
tweak for the data is computed as:
data_tweak = sha256(pk || data)
In the following, the current input is the input whose script is being executed.
The following value of the
<mode>
are defined:
CCV_MODE_CHECK_INPUT = -1
: Check an input's script; no
amount check.
CCV_MODE_CHECK_OUTPUT = 0
: Check an output's script;
preserve the (possibly residual) amount.
CCV_MODE_CHECK_OUTPUT_IGNORE_AMOUNT = 1
: Check an
output's script; ignore amounts.
CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT = 2
: Check an
output's script; deduct the output amount from the input's residual
amount.
Any other value of the
<mode>
makes the opcode
succeed validation immediately for the current input
5
.
The following values of the other parameters have special meanings:
<taptree>
is -1, it is replaced with the
Merkle root of the current input's tapscript tree. If the taptree is the
empty buffer, then the taptweak is skipped.
<pk>
is 0, it is replaced with the NUMS
x-only pubkey
0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
defined in
BIP 341
. If the
<pk>
is -1, it is
replaced with the taproot internal key of the current input.
<index>
is -1, it is replaced with the
index of the current input.
<data>
is the empty buffer, then there is
no data tweak for the input/output being checked.
Any other value of the
<taptree>
,
<pk>
,
<index>
or
<data>
parameters is invalid, and makes the opcode
immediately fail validation.
OP_CHECKCONTRACTVERIFY
The specification is divided into three parts:
The following helper function is a variant of
taproot_tweak_pubkey
from
BIP 341
, except that a regular
SHA256-hash is used instead of a tagged hash, and the pubkey is returned
unchanged if the length of
data
is 0.
def tweak_embed_data(pubkey, data):
assert len(pubkey) == 32
if len(data) == 0:
return None, pubkey
data_tweak = sha256(pubkey + data)
t = int_from_bytes(data_tweak)
if t >= SECP256K1_ORDER:
raise ValueError
P = lift_x(int_from_bytes(pubkey))
if P is None:
raise ValueError
Q = point_add(P, point_mul(G, t))
return 0 if has_even_y(Q) else 1, bytes_from_int(x(Q))
The
taproot_tweak_pubkey
function is also used as a
helper in the pseudocode below.
The following notations are used in the pseudocode below:
n_inputs
and
n_outputs
are the number of
inputs and outputs of the transaction, respectively;
inputs[i]
is the i-th input of the transaction;
outputs[i]
is the i-th output of the transaction;
this_input_index
,
this_input_internal_key
and
this_input_taptree
are the index,
taproot internal key and taproot Merkle root of the current input, respectively.
P2TR(key)
computes the scriptPubKey of the P2TR output
with the given key as the taproot output key.
This is executed once for the entire transaction, before any of the transaction input's scripts are evaluated. It initializes three arrays that are used to keep track of the amount information of the output.
output_min_amount = [0] * n_outputs
output_checked_default = [False] * n_outputs
output_checked_deduct = [False] * n_outputs
This is executed at the beginning of the evaluation of each input's script. It initializes the residual amount to equal the full amount of the current input.
residual_input_amount = input[this_input_index].amount
OP_CHECKCONTRACTVERIFY
evaluation
The following code is executed every time the
OP_CHECKCONTRACTVERIFY
opcode is encountered during the
evaluation of a taproot script spend.
this_input_index
,
this_input_internal_key
and
this_input_taptree
are the index, taproot internal key and taproot Merkle root of the
current input.
BIP341_NUMS_KEY
is the x-only provably unspendable key
50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
defined in
BIP 341
.
if mode < CCV_MODE_CHECK_INPUT or mode > CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT:
return success() # undefined mode is OP_SUCCESS
if index == -1:
index = this_input_index
if mode == CCV_MODE_CHECK_INPUT:
if index < 0 or index >= n_inputs:
return fail() # input index out of bounds
target_script = inputs[index].scriptPubKey
else:
if index < 0 or index >= n_outputs:
return fail() # output index out of bounds
target_script = outputs[index].scriptPubKey
if taptree == <-1>:
taptree = this_input_taptree
if pk == <0>:
naked_key = BIP341_NUMS_KEY
elif pk == <-1>:
naked_key = this_input_internal_key
elif len(pk) == 32:
naked_key = pk
else:
return fail()
# Verify the target contract data and program
_, internal_key = tweak_embed_data(naked_key, data)
if len(taptree) != 0:
if len(taptree) != 32:
return fail()
_, final_key = taproot_tweak_pubkey(internal_key, taptree)
else:
final_key = internal_key
if target_script != P2TR(final_key):
return fail()
# Amount checks
if mode == CCV_MODE_CHECK_OUTPUT:
# default amount semantic
if output_checked_deduct[index]:
return fail()
output_min_amount[index] += residual_input_amount
residual_input_amount = 0
if outputs[index].amount < output_min_amount[index]:
return fail()
output_checked_default[index] = True
elif mode == CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT:
# 'deduct' amount semantic
if residual_input_amount < outputs[index].amount:
return fail()
if output_checked_default[index] or output_checked_deduct[index]:
return fail()
residual_input_amount = residual_input_amount - outputs[index].amount
output_checked_deduct[index] = True
stack.pop(5) # drop all 5 stack elements
TODO
TODO
A reference implementation is provided as a bitcoin-core pull request .
This section documents some common script fragments that use
OP_CHECKCONTRACTVERIFY
for various common choices of the
parameters. Depending on the use case, some of the parameters might be
passed via the witness stack. In these examples,
<>
(empty buffer) and
0
both refer to an empty stack
element.
Check data embedded in the current input:
<data=data>
<index=-1>
<pk=naked_pk>
<taptree=-1>
<mode=-1 (CCV_MODE_CHECK_INPUT)>
OP_CHECKCONTRACTVERIFY
This would be used to access the data committed in the current input.
The
<data>
parameter, of course, would be passed via
the witness stack.
Check that the input with index
in_i
is a specific
contract with embedded
input_data
:
<data=input_data>
<index=in_i>
<pk=input_i_naked_pk>
<taptree=input_i_taptree>
<mode=-1 (CCV_MODE_CHECK_INPUT)>
OP_CHECKCONTRACTVERIFY
This allows introspecting the program (naked key and taptree) and data of another input of the transaction.
Check that the output with index
out_i
is a certain
contract (pubkey and taptree) with the specified embedded
<data>
, preserving input amount:
<data=data>
<index=out_i>
<pk=output_naked_pk>
<taptree=output_taptree>
<mode=0 (CCV_MODE_CHECK_OUTPUT)>
OP_CHECKCONTRACTVERIFY
This allows introspecting an output's program and data, and sending
the full residual amount to it. Logically, it can be thought as a state
transition, moving money to a different state, but still under the
control of the pre-set rules. Typically, the
data
would be
computed based on the witness stack, while the output program would be
hard-coded in Script.
Check that the output with index
out_i
is a P2TR with
pubkey
output_pk
, preserving amount:
<data=<>> # no data
<index=out_i>
<pk=output_pk>
<taptree=<>> # no taptweak
<mode=0 (CCV_MODE_CHECK_OUTPUT)>
OP_CHECKCONTRACTVERIFY
Unlike the previous example, here there is no computation over the
destination taproot public key, since both
data
and
taptweak
are omitted.
Check that the output with index
out_i
is a certain
contract (pubkey and taptree) with the specified embedded
data
; don't check amount:
<data=data>
<index=out_i>
<pk=output_i_naked_pk>
<taptree=output_i_taptree>
<mode=1 (CCV_MODE_CHECK_OUTPUT_IGNORE_AMOUNT)>
OP_CHECKCONTRACTVERIFY
Remark: amounts are malleable with this check alone; therefore, it is expected that the amount is also checked with separate introspection opcodes.
Check that the amount of the current input is partially sent to the first output (that must have a certain pubkey), and all the remaining amount is sent to the second output, which has the same internal key and taptree as the current input:
<data=<>> # no data
<index=0>
<pk=<output_0_pk>>
<taptree=<>> # no tweak
<mode=2 (CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT)>
OP_CHECKCONTRACTVERIFY
<data=<>> # no data
<index=1>
<pk=-1>
<taptree=-1>
<mode=0 (CCV_MODE_CHECK_OUTPUT)>
OP_CHECKCONTRACTVERIFY
Remark: in some applications, it might be desirable to check the exact amount of the first output with separate introspection opcodes.
TODO
The activation mechanism, and the set of other BIPs to be concurrently deployed, are to be determined.
OP_CHECKCONTRACTVERIFY
replaces the witness v1-only
opcode OP_SUCCESS187 with stricter verification semantics. Consequently,
scripts using those opcodes which previously were valid will cease to be
valid with this change.
Stricter verification semantics for an OP_SUCCESSx opcode are a soft fork, so existing software will be fully functional without upgrade except for mining and block validation.
TODO
<mode>
. As the mode would always be
hard-coded via a push in the script, the risk of mistakes seems
negligible.
↩︎