PR #216proposed changes toZIP 309

[WIP] [TZE] [ZIP 309] Add zkChannels (or BOLT) support

status → Draftby jakinyele · updated 2026-05-19zip-0309.rst @ 054d76f7fe · upstream 4699a8d518view on GitHub ↗

Unofficial preview — the proposed text below is rendered from the pull request head and is not part of the canonical ZIP set.

Terminology

The key words "MUST" and "MUST NOT" in this document are to be interpreted as described in RFC 2119. [2]

Abstract

This proposal specifies three possible approaches for integrating the zk-Channels protocol [#zk-Channels-paper]_ into Zcash.

Motivation

Layer 2 protocols like Lightning enable scalable payments for Bitcoin but lack the mechanisms to provide strong privacy guarantees on the payment network. Zcash offers private transactions but currently lacks features that would enable a Lightning-style payment channel. This proposal specifies an integration of the zkChannel privacy-preserving payment channel protocol on top of Zcash [#zk-Channels-paper]_.

zk-Channels high level Overview

zk-Channels enables two parties, a customer and a merchant, to open an assymetric anonymous payment channel where only the customer may initiate channel opening and payments. While the channel opening and closure are broadcast on-chain, making the channel's participants at most pseudonymous, the payments are done directly between the customer and merchant offchain in an anonymous and unlinkable fashion.

The channel opening can be split in three phases: initialisation, activation and unlink:

  1. in the initialisation phase, the customer and merchant set up the channel by agreeing on initial balances and auxiliary information.

  2. in the activation phase, the merchant ensures that the channel has been properly funded and sends to the client a payment token to allow the customer to transfer funds.

  3. the unlink phase goal is to unlink the channel opening with futher payments by performing a first unlinkable payment with null amount.

The off-chain payments is also split in three phases: setup, update state, and unmask.

  1. in the setup phase, the customer generates a new channel state privately and sends to the customer a payment request. The merchant checks checks this is not a double spend, and if so either accept or reject the new payment.

  2. in the update state phase, the customer and merchant participate in a zk-protocol where the merchant agrees on the new payment, by signing blindy on the new channel state, while revoking the current state.

  3. in the unmask phase, the customer and merchant gets the payment and revocation token to close the zk-channel onchain.

The channel can be closed in three different ways:

  • unilaterally by the merchant. In that case, the merchant uses the closing information exchanged in the channel initialisation to broadcast an expiry transaction onchain; the total amount is time-locked.

    1. during the time lock, the customer can broadcast a customer close which nullify the expiry transaction;

    2. after the time lock, the merchant can claim the funds.

  • unilaterally by the customer. In that case, the customer uses the close authorization message received in a update state phase to broadcase a customer close transaction onchain; the merchant balance is sent directly to the merchant while the customer balance is time-locked.

    1. during the time lock, the customer balance can be disputed by the merchant, in case the customer closed on an outdated state, by revealing the revocation secret. In that case, all funds go to the merchant;

    2. after the time lock, the customer can claim the funds.

  • mutually. In that case, the customer and merchant exchange information to generate a multisig transaction closing the channel.

1. Requirements

1.1 General requirements for zk-Channels protocol

zk-Channels private payment channels require the following capabilities to provide anonymity properties for users on a payment network:

  1. Ability to create an escrow transaction such that the transaction inputs are anonymous.

  2. Ability to escrow funds to a multi-signature style address via non-malleable transactions.

  3. Ability to specify relative time locks for commitment transactions to support unilateral channel closing.

  4. Ability to validate zk-Channels-specific opening and closing transactions:

  1. check the validity of randomized/blinded signature on the channel state in closing token;

  2. check the validity of revocation lock in the event of a channel dispute by merchant.

  1. Ability to enforce a transaction flow thanks to Transparent ZCash Extensions [#zip-0222]

  2. Ability to verify transaction outputs using TZEs such that:

  1. if customer-initiated closing, one output pays out to customer with a time lock (to allow merchant to dispute customer balance) and one output pays out to merchant immediately;

  2. if merchant-initiated closing, a single output pays the merchant the full balance of the channel with a time lock that allows for customer dispute.

Channel Operation Assumptions.
  • Channels funded by the customer alone and dual-funded channels are both supported.

  • Either the customer or the merchant can initiate channel closing.

  • If the customer initiates closing, then the merchant can dispute the closing transaction if they disagrees with the closing token in the closing transaction.

  • If the merchant initiates closing, the merchant posts a transaction claiming all the funds in the channel for themselves with a timelock. This gives the customer the opportunity to post their own valid closing transaction with the current channel balances. If the customer posts their own closing transaction, the merchant has an additional opportunity to dispute if necessary.

1.2 TZEs requirement for zk-Channels protocol

  1. Ability to check the sender of the current transaction;

  2. Ability to check the owner of an address;

  3. Ability to check the amount of an address;

  4. Ability to check the current block height;

  5. Ability to check information about a transaction of an input TZE address;

  6. Ability to verify Pointcheval-Sanders signatures on specific messages;

  7. Ability to assert a hash lock result;

2 Algorithms and variables

2.1 Customer and Merchant Signing Keys

The customer and the merchant both have key pairs from a suitable signature scheme to send and receive transactions. e.g. ECDSA or RedDSA signature key pairs. These are denoted as: <cust-pk>, <cust-sk> and <merch-pk>, <merch-sk>, respectively, where pk stands for "public key" and sk stands for the corresponding "secret key".

The merchant must be able to issue blind signatures, for instance Pointcheval-Sanders (PS) blind signature, so they have an additional keypair; this keypair is denoted as: <merch-ps-pk>, <merch-ps-sk>. To verify a PS signature, we will use the following function: PS_sig.Verify(merch-ps-pk, message, signature).

The customer key pair is specific to the channel and must not be reused. The merchant key pair is long term and should be used for all customer channels.

2.2 Channel States

zk-Channels relies on the concept of states. When the customer initiates a payment, they query the merchant to accept a new state while revoking the current one.

The only zk-channel's invariants are the channel identifier <cid>, and its participants' public keys. A channel state consists of:

  • the channel id: <cid>;

  • the merchant's public keys: <merch-pk>, <merch-ps-pk> (defined in 1.1);

  • the customer's public keys: <cust-pk> (defined in 1.1);

  • the customer's balance: <cust-balance>;

  • the merchant's balance: <merch-balance>;

  • the revocation lock: <rev-lock> which is instantiated as a hash lock, <rev-lock> = SHA256(<rev-secret>) where <rev-secret> is the revocation secret.

The revocation lock is needed by the merchant to dispute a customer close on an outdated state.

3 Opening a channel

To open a channel, the customer and merchant exchange key information and set the channel identifier <cid>.

They agree on their respective initial balances <cust-balance> and <merch-balance>.

The customer picks an inital revocation lock <rev-lock>.

The customer and merchant escrow the necessary funds in a funding transaction, denoted escrow-tx.

As such, the escrow transaction presents:

  • an opening channel state <state> = <cid>, <merch-pk>, <merch-ps-pk>, <cust-pk>, <merch-balance>, <cust-balance>, <rev-lock>;

  • potentially additional information such as a delay <delay>.

4 Closing a channel

A customer should be able to unilaterally close the channel by posting a close-tx transaction. This transaction presents:

  • a closing channel state <state> = <cid>, <merch-pk>, <merch-ps-pk>, <cust-pk>, <cust-balance>, <merch-balance>, <rev-lock>;

  • a unblinded signature from the merchant on the channel state: <merch-sig> such that PS_sig.Verify(<merch-ps-pk>, <state>, <merch-sig>) = 1.

If the transaction is validated and mined, <merch-balance> is sent to the merchant and <cust-balance> is locked for a delay <delay> during which the merchant can dispute the channel closing thanks to a dispute-tx transaction, and after which the customer can claim it thanks to a cust-claim-tx transaction.

The merchant can dispute a customer close by posting a dispute-tx transaction. This transaction presents:

  • a revocation secret <rev-secret> such that for the revocation lock <rev-lock> posted in close-tx we have <rev-lock> = SHA256(<rev-secret>).

If the transaction is validated and mined, the total balance (<cust-balance> + <merch-balance>) is sent to the merchant.

A merchant should be able to unilaterally close the channel by posting a closing transaction expiry-tx.

If the transaction is validated and mined, the total balance is locked for a delay <delay> during which the customer can overwrite the spend with a close-tx transaction, and after which the merchant can claim it thanks to a merch-claim-tx transaction.

The customer and merchant may also negotiate off-chain to form a mutual close transaction, mutual-close-tx. This transaction presents:

  • a truncated closing channel state <state> without the revocation lock and the merchant's blind signature public key;

  • two signatures from the customer <cust-sig> and the merchant <merch-sig> on this truncated state such that Verify(<cust-pk>, <state>, <cust-sig>) = 1 and Verify(<merch-ps-pk>, <state>, <merch-sig>) = 1.

If the transaction is validated and mined, the balance <cust-balance> is sent to the customer and the balance <merch-balance> is sent to the merchant.

zk-Channels TZE transactions

We present in this section the individual transactions for opening and closing a zk-Channel as well as disputing and claiming funds of a zkChannel.

For simplicity, we describe the minimal information needed to understand the workflow, that is we show only the relevant part of the verify_tze functions and do not detail more than necessary the transaction structure.

1 Escrow transaction

We present here the escrow transaction escrow-tx:

Field

Value

...

...

tx_in_count

2

tx_in

[<cust-input>, <merch-input>]

tx_out_count

0

tx_out

[]

tze_in_count

0

tze_in

[]

tze_out_count

1

tze_out

[<escrow-out>]

lock_time

0

... ...

With,

  • <cust-input> being a transparent address comprising an initial amount of <cust-balance>.

  • <merch-input> being a transparent address comprising an initial amount of <merch-balance>.

  • with <escrow-out> being a tze input:

    • amount = <merch-balance> + <cust-balance>

    • precondition =

      • tze_id = 0 // zk-Channels TZE type

      • tze_mode = 0 // escrow mode

      • tze_data_payload_len =

      • tze_data_payload = <cid> || <merch-pk> || <merch-ps-pk> || <cust-pk>

An escrow transaction can either be spent by a mutual close, merchant expiry or customer close transaction.

2 Mutual close

We present here the mutual close transaction mutual-close-tx:

Field

Value

...

...

tx_in_count

0

tx_in

[]

tx_out_count

2

tx_out

[<merch-output>, <cust-output>]

tze_in_count

1

tze_in

[<escrow-in>]

tze_out_count

0

tze_out

[]

lock_time

0

... ...

With,
  • <escrow-in>:

    • prevout_hash = <escrow-tx>

    • prevout_in = 0

    • witness :

      • tze_id = 0 // zkChannels TZE type

      • tze_mode = 0 // escrow mode>

      • tze_data_payload_len =

      • tze_data_payload = <merch-sig> || <cust-sig>

  • <merch-output> being a transparent address comprising an amount of <merch-balance>;

  • <cust-output> being a transparent address comprising an amount of <cust-balance>.

The tze_verify function works as follow:

tze_verify(mode, precondition, witness):

  • ...

  • if mode == 0 AND context.tx.tx_out_counts == 2:

    • // Check the transaction's structure

    • if context.tx.tx_in_count != 0 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0:

      • Return false

    • // Define the variables

    • cid || merch-pk || merch-ps-pk || cust-pk = precondition

    • merch-sig || cust-sig = witness

    • msg = TODO // hash of tx without witnesses

    • // Check the tx signatures

    • if Verify(merch-pk, msg, merch-sig) != 1

      • Return False

    • if Verify(cust-pk, msg, cust-sig) != 1

      • Return False

    • Return True

  • ...

3 Expiry

3.1 Expiry

We present here the unilateral merchant close transaction, also called expiry, expiry-tx:

Field

Value

...

...

tx_in_count

0

tx_in

[]

tx_out_count

0

tx_out

[]

tze_in_count

1

tze_in

[<escrow-in>]

tze_out_count

1

tze_out

[<expiry-out>]

lock_time

0

... ...

With,

  • <escrow-in>:

    • prevout_hash = <escrow-tx>

    • prevout_in = 0

    • witness:

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 0 // escrow mode

      • tze_data_payload_len =

      • tze_data_payload = <merch-sig>

  • <expiry-out>:

    • amount = <merch-balance> + <cust-balance>

    • precondition:

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 2 // expiry mode

      • tze_data_payload_len =

      • tze_data_payload = <cid> || <merch-pk> || <merch-ps-pk> || <cust-pk>

The tze_verify functions works as follows:

tze_verify(mode, precondition, witness):

  • ...

  • if mode == 0 AND context.tx.tze_out_count == 1 AND context.tx.tx_out[0].precondition.mode == EXPIRY:

    • // Check the transaction's structure

    • if context.tx.tx_in_count != 0 OR context.tx.tx_out_count != 0 OR context.tx.tze_in_count != 1 OR context.tx.nShieldedSpend !=0 OR context.tx.nShildedOutput != 0 OR context.tx.nJoinsplit != 0:

    • Return false

    • // Define the variables

    • init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition

    • merch-sig = witness

    • cid || merch-pk || merch-ps-pk || cust-pk = context.tx.tze_out[0].precondition.tze_data_payload

    • msg = TODO // hash of tx without witnesses

    • // Check the variables consistency

    • if init-cid != cid:

      • Return False

    • if init-merch-pk != merch-pk:

      • Return False

    • if init-merch-ps-pk != merch-ps-pk:

      • Return False

    • if init-cust-pk != cust-pk:

      • Return False

    • // Verify the tx signature

    • if Verify(merch-pk, msg, merch-sig) = 0

      • Return False

    • Return True

  • ...

3.2 Merchant claim

We present here the merchant claim transaction merch-claim-tx following a expiry-tx transaction.

Field

Value

...

...

tx_in_count

0

tx_in

[]

tx_out_count

1

tx_out

[<merch-claim]

tze_in_count

1

tze_in

[<expiry-in>]

tze_out_count

0

tze_out

[]

lock_time

0

... ...

With,

  • <expiry-in>:

    • prevout_hash = <expiry-tx>

    • prevout_in = 0

    • witness:

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 2 // expiry mode

      • tze_data_payload_len =

      • tze_data_payload = <merch-sig>

    • <merch-claim> being a transparent address comprising an amount of <cust-balance> + <merch-balance>.

The tze_verify function works as follow:

tze_verify(type, mode, precondition, witness)

  • ...

  • if mode == 2:

    • // Check the transaction's structure

    • if context.tx.tx_in_count != 0 OR context.tx.tx_in_count != 1 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0: - Return false

    • // Define the variables

    • cid || merch-pk || merch-ps-pk || cust-pk = precondition

    • sig = witness

    • msg = TODO // hash of the tx without witnesses

    • // Verify the tx signature

    • if Verify(merch-pk, msg, sig) == 1:

      • // Check the delay

      • expiry-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO

      • if context.block-height < expiry-height + DELAY:

        • Return False

      • Return True

  • ...

4 Customer close

4.1 Customer close

We present here the customer close transaction close-tx following either a expiry-tx or a escrow-tx transaction.

Field

Value

...

...

tx_in_count

0

tx_in

[]

tx_out_count

1

tx_out

[<merch-output]

tze_in_count

1

tze_in

[<expiry-in> OR <escrow-in>]

tze_out_count

1

tze_out

[<close-out]

lock_time

0

... ...

With,

  • <escrow-in>:

    • prevout_hash = <escrow-tx>

    • prevout_in = 0

    • witness:

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 0 // escrow mode

      • tze_data_payload_len =

      • tze_data_payload = <merch-ps-sig> || <cust-sig>

  • <expiry-in>:

    • prevout_hash = <escrow-tx>

    • prevout_in = 0

    • witness :

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 2 // expiry mode

      • tze_data_payload_len =

      • tze_data_payload = <merch-ps-sig> || <cust-sig>

  • <merch-output> being a transaprent address comprising an amount of <merch-balance>;

  • <close-out>:

    • amount = <cust-balance>

    • precondition:

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 1 // close mode

      • tze_data_payload_len =

      • tze_data_payload = <cid> || <merch-pk> || <cust-pk> || <rev-lock>

The tze_verify function works as follow:

tze_verify(mode, precondition, witness)

  • ...

  • if (mode == 0 OR mode == 2) AND context.tx.tze_out_count == 1 AND context.tx.tze_out[0].precondition.mode = CLOSE:

    • // Check the transaction's structure

    • if context.tx.tx_in_count != 0 OR context.tx.tx_out_count != 1 OR context.tx.tze_in_count != 1 OR context.tx.nShieldedSpend !=0 OR context.tx.nShildedOutput != 0 OR context.tx.nJoinsplit != 0: - Return false

    • // Define the variables

    • init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition

    • merch-ps-sig || cust-sig = witness

    • cid || merch-pk || cust-pk || rev-lock = context.tx.tze_out[0].precondition.tze_data_payload

    • merch-balance = context.tx.tx_out[0].value

    • cust-balance = context.tx.tze_out[0].amount

    • state = cid || cust-pk || rev-lock || cust-balance || merch-balance

    • msg = TODO // hash of tx without witnesses

    • // Check the variables' consistency

    • if init-cid != cid:

      • Return False

    • if init-cust-pk != cust-pk:

      • Return False

    • if init-merch-pk != merch-pk:

      • Return False

    • // Verify the PS signature

    • if PS_sig.Verify(merch-ps-pk, state, merch-ps-sig) != 1:

      • Return False

    • // Verify the tx signature

    • if Verify(cust-pk, msg, cust-sig) != 1:

      • Return False

    • // Check merch-balance is sent to the merchant

    • if context.tx.tx_out[0] != merch-balance || <ZCash P2PKH version bytes (0x1C || 0xB8 for mainnet)> || Hash_160(merch-pk)

      • Return False

    • Return True

  • ...

4.2 Customer claim

We present here the customer claim transaction cust-claim-tx following a close-tx transaction.

Field

Value

...

...

tx_in_count

0

tx_in

[]

tx_out_count

1

tx_out

[<cust-claim]

tze_in_count

1

tze_in

[<close-in>]

tze_out_count

0

tze_out

[]

lock_time

0

... ...

With,

  • <close-in>:

    • prevout_hash = <close-tx>

    • prevout_in = 0

    • witness:

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 1 // close mode

      • tze_data_payload_len =

      • tze_data_payload = <cust-sig>

    • <cust-claim> being a transparent address comprising an amount of <cust-balance>.

The tze_verify function works as follow:

tze_verify(type, mode, precondition, witness)

  • ...

  • if mode == 1:

    • // Check the transaction's structure

    • if context.tx.tx_in_count != 0 OR context.tx.tx_out_count == 1 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0:

      • Return false

    • // Define the variables

    • cid, merch-pk, cust-pk, rev-lock = precondition

    • sig = witness

    • msg = TODO // hash of tx without witnesses

    • // Check the tx signature

    • if Verify(cust-pk, msg, sig) == 1:

      • // Check the delay

      • close-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO

      • if context.block-height < close-height + DELAY:

        • Return False

      • Return True

  • ...

4.3 Dispute

We present here how the merchant dispute the transaction close-tx with a dispute-tx transaction.

Field

Value

...

...

tx_in_count

0

tx_in

[]

tx_out_count

1

tx_out

[<merch-disp]

tze_in_count

1

tze_in

[<close-in>]

tze_out_count

0

tze_out

[]

lock_time

0

... ...

With,

  • <close-in>:

    • prevout_hash = <close-tx>

    • prevout_in = 0

    • witness:

      • tze_id = 0 // zkChannels' TZE type

      • tze_mode = 1 // close mode

      • tze_data_payload_len =

      • tze_data_payload = <merch-sig> || <rev-secret>

    • <merch-disp> being a transparent address comprising an amount of <cust-balance>.

The tze_function works as follow:

tze_verify(zkChannels-type, close-mode, precondition, witness)

  • ...

  • if mode == 1:

    • // Check the transaction's structure

    • if context.tx.tx_in_count != 0 OR context.tx.tx_out_count == 1 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0:

      • Return false

    • // Define the variables

    • cid, merch-pk, cust-pk, rev-lock = precondition

    • sig = witness

    • msg = TODO // hash of tx without witnesses

    • // Check the tx signature

    • if Verify(merch-pk, msg, sig) == 1:

      • // Check the hash lock

      • if rev-lock != SHA256(rev-secret):

        • Return False

      • Return True

  • ...

TZE_VERIFY function

tze_verify(mode, precondition, witness):

  • // Check the transaction structure

  • if context.tx.tx_in_count != 0 OR context.tx.tze_in_count != 1 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0:

    • Return False

  • msg = TODO // hash of tx without witnesses

  • if mode == 0: // escrow mode

    • init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition

    • if context.tx.tze_out_count == 1:

      • if context.tx.tze_out[0].precondition.tze_mode == 1: // This is a close transaction

        • // Check the transaction's structure

        • if context.tx.tx_out_count != 1:

          • Return False

        • merch-ps-sig || cust-sig = witness

        • cid || merch-pk || cust-pk || rev-lock = context.tx.tze_out[0].precondition.tze_data_payload

        • merch-balance = context.tx.tx_out[0].value

        • cust-balance = context.tx.tze_out[0].amount

        • state = cid || cust-pk || rev-lock || cust-balance || merch-balance

        • if init-cid != cid OR init-merch-pk != merch-pk OR init-cust-pk != cust-pk:

          • Return False

        • if Verify(cust-pk, msg, cust-sig) != 1 OR PS_sig.Verify(merch-ps-pk, state, merch-ps-sig) != 1:

          • Return False

        • if context.tx.tx_out[0] == merch-balance || <ZCash P2PKH version bytes (0x1C || 0xB8 for mainnet)> || Hash_160(merch-pk)

          • Return True

      • if context.tx.tze_out[0].precondition.tze_mode == 2: // This is an expiry transaction

        • // Check the transaction's structure

        • if context.tx.tx_out_count != 0:

          • Return False

        • merch-sig = witness

        • cid || merch-pk || merch-ps-pk || cust-pk = context.tx.tze_out[0].precondition.tze_data_payload

        • if init-cid != cid OR init-merch-pk != merch-pk OR init-merch-ps-pk != merch-ps-pk OR init-cust-pk != cust-pk:

          • Return False

        • if Verify(merch-pk, msg, merch-sig) == 1:

          • Return True

    • if context.tx.tze_out_count == 0 AND if context.tx.tx_out_count == 2: // This is a mutual close transaction

      • merch-sig || cust-sig = witness

      • if Verify(init-merch-pk, msg, merch-sig) == 1 AND Verify(init-cust-pk, msg, cust-sig) == 1:

        • Return True

  • else if mode == 1: // close mode

    • cid, merch-pk, cust-pk, rev-lock = precondition

    • sig, (rev-secret) = witness

    • if context.tx.tx_out.count == 1:

      • // Check the transaction's structure

      • if context.tx.tze_out_count != 0:

        • Return False

      • if Verify(cust-pk, msg, sig) == 1: // This is a customer claim transaction

        • expiry-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO

        • if context.block-height >= expiry-height + DELAY:

          • Return True

      • if Verify(merch-pk, msg, sig) == 1:// This is a dispute transaction

        • if rev-lock == SHA256(rev-secret):

          • Return True

  • else if mode == 2: // expiry mode

    • init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition

    • if context.tx.tze_out_count == 1: // This is a close transaction

      • // Check the transaction's structure

      • if context.tx.tx_out_count != 1:

        • Return False

      • merch-ps-sig || cust-sig = witness

      • cid || merch-pk || cust-pk || rev-lock = context.tx.tze_out[0].precondition.tze_data_payload

      • merch-balance = context.tx.tx_out[0].value

      • cust-balance = context.tx.tze_out[0].amount

      • state = cid || cust-pk || rev-lock || cust-balance || merch-balance

      • if init-cid != cid OR init-merch-pk != merch-pk OR init-cust-pk != cust-pk:

        • Return False

      • if Verify(cust-pk, msg, cust-sig) != 1 OR PS_sig.Verify(merch-ps-pk, state, merch-ps-sig) != 1:

        • Return False

      • if context.tx.tx_out[0] == merch-balance || <ZCash P2PKH version bytes (0x1C || 0xB8 for mainnet)> || Hash_160(merch-pk)

        • Return True

    • else if context.tx.tx_out_count == 1: // This is a merchant claim transaction

      • sig = witness

      • if Verify(merch-pk, msg, sig) == 1:

        • expiry-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO

        • if context.block-height >= expiry-height + DELAY:

          • Return True

  • Return False

Transaction Malleability

As the transaction id is not built upon the TZE witnesses, any change to the transaction's signature does not modify its id.

The preconditions are non-malleable as they are signed with a non-malleable signature.

To prevent witness malleability, we intend to fix the witness payload's size in the tze_verify such that the addition or removal of any bit would result in a verification failure. Any change to the bits should either result in a verification failure, when the revocation secret or a signature is corrupted, or a pass, when the PS signature is randomized.

References

Docutils System Messages

Changes

zip-0309.rst +858 −0
@@ -0,0 +1,858 @@ +:: + + ZIP: 309 + Title: Add support for zk-Channels protocol + Authors: J. Ayo Akinyele <ayo@boltlabs.io> + Colleen Swanson <swan@boltlabs.io> + Raphael Toledo <contact@raphael-toledo.com> + Credits: Ian Miers <imiers@z.cash> + Matthew Green <mgreen@z.cash> + Category: Informational + Created: 2019-07-15 + License: MIT + + +Terminology +=========== + +The key words "MUST" and "MUST NOT" in this document are to be interpreted as described in RFC 2119. [#RFC2119]_ + +Abstract +======== + +This proposal specifies three possible approaches for integrating the zk-Channels protocol [#zk-Channels-paper]_ into Zcash. + +Motivation +========== + +Layer 2 protocols like Lightning enable scalable payments for Bitcoin but lack the mechanisms to provide strong privacy guarantees on the payment network. Zcash offers private transactions but currently lacks features that would enable a Lightning-style payment channel. This proposal specifies an integration of the zkChannel privacy-preserving payment channel protocol on top of Zcash [#zk-Channels-paper]_. + +zk-Channels high level Overview +=============================== + +zk-Channels enables two parties, a customer and a merchant, to open an assymetric anonymous payment channel where only the customer may initiate channel opening and payments. While the channel opening and closure are broadcast on-chain, making the channel's participants at most pseudonymous, the payments are done directly between the customer and merchant offchain in an anonymous and unlinkable fashion. + +The channel opening can be split in three phases: initialisation, activation and unlink: + +1. in the initialisation phase, the customer and merchant set up the channel by agreeing on initial balances and auxiliary information. +2. in the activation phase, the merchant ensures that the channel has been properly funded and sends to the client a payment token to allow the customer to transfer funds. +3. the unlink phase goal is to unlink the channel opening with futher payments by performing a first unlinkable payment with null amount. + +The off-chain payments is also split in three phases: setup, update state, and unmask. + +1. in the setup phase, the customer generates a new channel state privately and sends to the customer a payment request. The merchant checks checks this is not a double spend, and if so either accept or reject the new payment. +2. in the update state phase, the customer and merchant participate in a zk-protocol where the merchant agrees on the new payment, by signing blindy on the new channel state, while revoking the current state. +3. in the unmask phase, the customer and merchant gets the payment and revocation token to close the zk-channel onchain. + +The channel can be closed in three different ways: + +- unilaterally by the merchant. In that case, the merchant uses the closing information exchanged in the channel initialisation to broadcast an expiry transaction onchain; the total amount is time-locked. + + a. during the time lock, the customer can broadcast a customer close which nullify the expiry transaction; + b. after the time lock, the merchant can claim the funds. + +- unilaterally by the customer. In that case, the customer uses the close authorization message received in a update state phase to broadcase a customer close transaction onchain; the merchant balance is sent directly to the merchant while the customer balance is time-locked. + + a. during the time lock, the customer balance can be disputed by the merchant, in case the customer closed on an outdated state, by revealing the revocation secret. In that case, all funds go to the merchant; + b. after the time lock, the customer can claim the funds. + +- mutually. In that case, the customer and merchant exchange information to generate a multisig transaction closing the channel. + +1. Requirements +--------------- + +1.1 General requirements for zk-Channels protocol +------------------------------------------------- + +zk-Channels private payment channels require the following capabilities to provide anonymity properties for users on a payment network: + +1. Ability to create an escrow transaction such that the transaction inputs are anonymous. +2. Ability to escrow funds to a multi-signature style address via non-malleable transactions. +3. Ability to specify relative time locks for commitment transactions to support unilateral channel closing. +4. Ability to validate zk-Channels-specific opening and closing transactions: + + 1. check the validity of randomized/blinded signature on the channel state in closing token; + + 2. check the validity of revocation lock in the event of a channel dispute by merchant. + +5. Ability to enforce a transaction flow thanks to Transparent ZCash Extensions [#zip-0222] +6. Ability to verify transaction outputs using TZEs such that: + + 1. if customer-initiated closing, one output pays out to customer with a time lock (to allow merchant to dispute customer balance) and one output pays out to merchant immediately; + + 2. if merchant-initiated closing, a single output pays the merchant the full balance of the channel with a time lock that allows for customer dispute. + +**Channel Operation Assumptions.** + - Channels funded by the customer alone and dual-funded channels are both supported. + - Either the customer or the merchant can initiate channel closing. + - If the customer initiates closing, then the merchant can dispute the closing transaction if they disagrees with the closing token in the closing transaction. + - If the merchant initiates closing, the merchant posts a transaction claiming all the funds in the channel for themselves with a timelock. This gives the customer the opportunity to post their own valid closing transaction with the current channel balances. If the customer posts their own closing transaction, the merchant has an additional opportunity to dispute if necessary. + +1.2 TZEs requirement for zk-Channels protocol +--------------------------------------------- + +1. Ability to check the sender of the current transaction; +2. Ability to check the owner of an address; +3. Ability to check the amount of an address; +4. Ability to check the current block height; +5. Ability to check information about a transaction of an input TZE address; +6. Ability to verify Pointcheval-Sanders signatures on specific messages; +7. Ability to assert a hash lock result; + +2 Algorithms and variables +---------------------------- + +2.1 Customer and Merchant Signing Keys +-------------------------------------- + +The customer and the merchant both have key pairs from a suitable signature scheme to send and receive transactions. e.g. ECDSA or RedDSA signature key pairs. These are denoted as: ``<cust-pk>``, ``<cust-sk>`` and ``<merch-pk>``, ``<merch-sk>``, respectively, where ``pk`` stands for "public key" and ``sk`` stands for the corresponding "secret key". + +The merchant must be able to issue blind signatures, for instance Pointcheval-Sanders (PS) blind signature, so they have an additional keypair; this keypair is denoted as: +``<merch-ps-pk>``, ``<merch-ps-sk>``. To verify a PS signature, we will use the following function: ``PS_sig.Verify(merch-ps-pk, message, signature)``. + +The customer key pair is specific to the channel and must not be reused. The merchant key pair is long term and should be used for all customer channels. + +2.2 Channel States +------------------ + +zk-Channels relies on the concept of *states*. When the customer initiates a payment, they query the merchant to accept a new state while revoking the current one. + +The only zk-channel's invariants are the channel identifier ``<cid>``, and its participants' public keys. A channel state consists of: + +- the channel id: ``<cid>``; +- the merchant's public keys: ``<merch-pk>, <merch-ps-pk>`` (defined in 1.1); +- the customer's public keys: ``<cust-pk>`` (defined in 1.1); +- the customer's balance: ``<cust-balance>``; +- the merchant's balance: ``<merch-balance>``; +- the revocation lock: ``<rev-lock>`` which is instantiated as a hash lock, ``<rev-lock> = SHA256(<rev-secret>)`` where ``<rev-secret>`` is the revocation secret. + +The revocation lock is needed by the merchant to dispute a customer close on an outdated state. + + +3 Opening a channel +------------------- + +To open a channel, the customer and merchant exchange key information and set the channel identifier ``<cid>``. + +They agree on their respective initial balances ``<cust-balance>`` and ``<merch-balance>``. + +The customer picks an inital revocation lock ``<rev-lock>``. + +The customer and merchant escrow the necessary funds in a funding transaction, denoted ``escrow-tx``. + +As such, the escrow transaction presents: + +- an opening channel state ``<state> = <cid>, <merch-pk>, <merch-ps-pk>, <cust-pk>, <merch-balance>, <cust-balance>, <rev-lock>``; +- potentially additional information such as a delay <delay>. + +4 Closing a channel +--------------------- + +A customer should be able to unilaterally close the channel by posting a ``close-tx`` transaction. This transaction presents: + +- a closing channel state ``<state> = <cid>, <merch-pk>, <merch-ps-pk>, <cust-pk>, <cust-balance>, <merch-balance>, <rev-lock>``; +- a unblinded signature from the merchant on the channel state: ``<merch-sig>`` such that ``PS_sig.Verify(<merch-ps-pk>, <state>, <merch-sig>) = 1``. + +If the transaction is validated and mined, <merch-balance> is sent to the merchant and <cust-balance> is locked for a delay <delay> during which the merchant can dispute the channel closing thanks to a ``dispute-tx`` transaction, and after which the customer can claim it thanks to a ``cust-claim-tx`` transaction. + + + +The merchant can dispute a customer close by posting a ``dispute-tx`` transaction. This transaction presents: + +- a revocation secret ``<rev-secret>`` such that for the revocation lock ``<rev-lock>`` posted in ``close-tx`` we have ``<rev-lock> = SHA256(<rev-secret>)``. + +If the transaction is validated and mined, the total balance (<cust-balance> + <merch-balance>) is sent to the merchant. + + + +A merchant should be able to unilaterally close the channel by posting a closing transaction ``expiry-tx``. + +If the transaction is validated and mined, the total balance is locked for a delay <delay> during which the customer can overwrite the spend with a ``close-tx`` transaction, and after which the merchant can claim it thanks to a ``merch-claim-tx`` transaction. + + + +The customer and merchant may also negotiate off-chain to form a *mutual close transaction*, ``mutual-close-tx``. This transaction presents: + +- a truncated closing channel state ``<state>`` without the revocation lock and the merchant's blind signature public key; +- two signatures from the customer ``<cust-sig>`` and the merchant ``<merch-sig>`` on this truncated state such that ``Verify(<cust-pk>, <state>, <cust-sig>) = 1`` and ``Verify(<merch-ps-pk>, <state>, <merch-sig>) = 1``. + +If the transaction is validated and mined, the balance <cust-balance> is sent to the customer and the balance <merch-balance> is sent to the merchant. + + +zk-Channels TZE transactions +=============================== +We present in this section the individual transactions for opening and closing a zk-Channel as well as disputing and claiming funds of a zkChannel. + +For simplicity, we describe the minimal information needed to understand the workflow, that is we show only the relevant part of the verify_tze functions and do not detail more than necessary the transaction structure. + +1 Escrow transaction +-------------------- +We present here the escrow transaction ``escrow-tx``: + + +============= ============================== +Field Value +============= ============================== +... ... +tx_in_count 2 +tx_in [<cust-input>, <merch-input>] +tx_out_count 0 +tx_out [] +tze_in_count 0 +tze_in [] +tze_out_count 1 +tze_out [<escrow-out>] +lock_time 0 +... ... +============================================ + +With, + +- <cust-input> being a transparent address comprising an initial amount of <cust-balance>. +- <merch-input> being a transparent address comprising an initial amount of <merch-balance>. +- with <escrow-out> being a tze input: + + - amount = <merch-balance> + <cust-balance> + - precondition = + + - tze_id = 0 // *zk-Channels TZE type* + - tze_mode = 0 // *escrow mode* + - tze_data_payload_len = + - tze_data_payload = <cid> || <merch-pk> || <merch-ps-pk> || <cust-pk> + + +An escrow transaction can either be spent by a mutual close, merchant expiry or customer close transaction. + +2 Mutual close +-------------- +We present here the mutual close transaction ``mutual-close-tx``: + +============= =============================== +Field Value +============= =============================== +... ... +tx_in_count 0 +tx_in [] +tx_out_count 2 +tx_out [<merch-output>, <cust-output>] +tze_in_count 1 +tze_in [<escrow-in>] +tze_out_count 0 +tze_out [] +lock_time 0 +... ... +============================================= + + +With, + - <escrow-in>: + + - prevout_hash = <escrow-tx> + - prevout_in = 0 + - witness : + + - tze_id = 0 // *zkChannels TZE type* + - tze_mode = 0 // *escrow mode>* + - tze_data_payload_len = + - tze_data_payload = <merch-sig> || <cust-sig> + + - <merch-output> being a transparent address comprising an amount of <merch-balance>; + - <cust-output> being a transparent address comprising an amount of <cust-balance>. + +The tze_verify function works as follow: + +**tze_verify(mode, precondition, witness):** + +- ... +- if mode == 0 AND context.tx.tx_out_counts == 2: + + - // *Check the transaction's structure* + - if context.tx.tx_in_count != 0 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0: + + - Return false + + + - // *Define the variables* + - cid || merch-pk || merch-ps-pk || cust-pk = precondition + - merch-sig || cust-sig = witness + - msg = TODO // *hash of tx without witnesses* + + + - // *Check the tx signatures* + + - if Verify(merch-pk, msg, merch-sig) != 1 + + - Return False + + - if Verify(cust-pk, msg, cust-sig) != 1 + + - Return False + + - Return True +- ... + + + +3 Expiry +-------- + +3.1 Expiry +------------------ + +We present here the unilateral merchant close transaction, also called expiry, ``expiry-tx``: + +============= =============================== +Field Value +============= =============================== +... ... +tx_in_count 0 +tx_in [] +tx_out_count 0 +tx_out [] +tze_in_count 1 +tze_in [<escrow-in>] +tze_out_count 1 +tze_out [<expiry-out>] +lock_time 0 +... ... +============================================= + +With, + +- <escrow-in>: + + - prevout_hash = <escrow-tx> + - prevout_in = 0 + + - witness: + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 0 // *escrow mode* + - tze_data_payload_len = + - tze_data_payload = <merch-sig> + +- <expiry-out>: + + - amount = <merch-balance> + <cust-balance> + - precondition: + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 2 // *expiry mode* + - tze_data_payload_len = + - tze_data_payload = <cid> || <merch-pk> || <merch-ps-pk> || <cust-pk> + +The tze_verify functions works as follows: + +**tze_verify(mode, precondition, witness):** + +- ... +- if mode == 0 AND context.tx.tze_out_count == 1 AND context.tx.tx_out[0].precondition.mode == EXPIRY: + + - // *Check the transaction's structure* + - if context.tx.tx_in_count != 0 OR context.tx.tx_out_count != 0 OR context.tx.tze_in_count != 1 OR context.tx.nShieldedSpend !=0 OR context.tx.nShildedOutput != 0 OR context.tx.nJoinsplit != 0: + - Return false + + - // *Define the variables* + - init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition + - merch-sig = witness + - cid || merch-pk || merch-ps-pk || cust-pk = context.tx.tze_out[0].precondition.tze_data_payload + - msg = TODO // *hash of tx without witnesses* + + - // *Check the variables consistency* + - if init-cid != cid: + + - Return False + + - if init-merch-pk != merch-pk: + + - Return False + + - if init-merch-ps-pk != merch-ps-pk: + + - Return False + + - if init-cust-pk != cust-pk: + + - Return False + + - // *Verify the tx signature* + - if Verify(merch-pk, msg, merch-sig) = 0 + + - Return False + + - Return True + +- ... + +3.2 Merchant claim +------------------ + +We present here the merchant claim transaction ``merch-claim-tx`` following a ``expiry-tx`` transaction. + +============= =============================== +Field Value +============= =============================== +... ... +tx_in_count 0 +tx_in [] +tx_out_count 1 +tx_out [<merch-claim] +tze_in_count 1 +tze_in [<expiry-in>] +tze_out_count 0 +tze_out [] +lock_time 0 +... ... +============================================= + +With, + +- <expiry-in>: + + - prevout_hash = <expiry-tx> + - prevout_in = 0 + - witness: + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 2 // *expiry mode* + - tze_data_payload_len = + - tze_data_payload = <merch-sig> + + - <merch-claim> being a transparent address comprising an amount of <cust-balance> + <merch-balance>. + + +The tze_verify function works as follow: + +**tze_verify(type, mode, precondition, witness)** + +- ... +- if mode == 2: + + - // *Check the transaction's structure* + - if context.tx.tx_in_count != 0 OR context.tx.tx_in_count != 1 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0: + - Return false + + - // *Define the variables* + - cid || merch-pk || merch-ps-pk || cust-pk = precondition + - sig = witness + - msg = TODO // *hash of the tx without witnesses* + + - // *Verify the tx signature* + - if Verify(merch-pk, msg, sig) == 1: + + - // *Check the delay* + - expiry-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO + - if context.block-height < expiry-height + DELAY: + + - Return False + + - Return True + +- ... + +4 Customer close +---------------- + +4.1 Customer close +------------------ + +We present here the customer close transaction ``close-tx`` following either a ``expiry-tx`` or a ``escrow-tx`` transaction. + + +============= =============================== +Field Value +============= =============================== +... ... +tx_in_count 0 +tx_in [] +tx_out_count 1 +tx_out [<merch-output] +tze_in_count 1 +tze_in [<expiry-in> OR <escrow-in>] +tze_out_count 1 +tze_out [<close-out] +lock_time 0 +... ... +============================================= + +With, + +- <escrow-in>: + + - prevout_hash = <escrow-tx> + - prevout_in = 0 + - witness: + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 0 // *escrow mode* + - tze_data_payload_len = + - tze_data_payload = <merch-ps-sig> || <cust-sig> + +- <expiry-in>: + + - prevout_hash = <escrow-tx> + - prevout_in = 0 + - witness : + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 2 // *expiry mode* + - tze_data_payload_len = + - tze_data_payload = <merch-ps-sig> || <cust-sig> + +- <merch-output> being a transaprent address comprising an amount of <merch-balance>; +- <close-out>: + + - amount = <cust-balance> + - precondition: + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 1 // *close mode* + - tze_data_payload_len = + - tze_data_payload = <cid> || <merch-pk> || <cust-pk> || <rev-lock> + + +The tze_verify function works as follow: + +**tze_verify(mode, precondition, witness)** + +- ... +- if (mode == 0 OR mode == 2) AND context.tx.tze_out_count == 1 AND context.tx.tze_out[0].precondition.mode = CLOSE: + + - // *Check the transaction's structure* + - if context.tx.tx_in_count != 0 OR context.tx.tx_out_count != 1 OR context.tx.tze_in_count != 1 OR context.tx.nShieldedSpend !=0 OR context.tx.nShildedOutput != 0 OR context.tx.nJoinsplit != 0: + - Return false + + - // *Define the variables* + - init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition + - merch-ps-sig || cust-sig = witness + - cid || merch-pk || cust-pk || rev-lock = context.tx.tze_out[0].precondition.tze_data_payload + - merch-balance = context.tx.tx_out[0].value + - cust-balance = context.tx.tze_out[0].amount + - state = cid || cust-pk || rev-lock || cust-balance || merch-balance + - msg = TODO // *hash of tx without witnesses* + + - // *Check the variables' consistency* + - if init-cid != cid: + + - Return False + + - if init-cust-pk != cust-pk: + + - Return False + + - if init-merch-pk != merch-pk: + + - Return False + + + - // *Verify the PS signature* + - if PS_sig.Verify(merch-ps-pk, state, merch-ps-sig) != 1: + + - Return False + + - // *Verify the tx signature* + + - if Verify(cust-pk, msg, cust-sig) != 1: + + - Return False + + - // *Check merch-balance is sent to the merchant* + - if context.tx.tx_out[0] != merch-balance || <ZCash P2PKH version bytes (0x1C || 0xB8 for mainnet)> || Hash_160(merch-pk) + + - Return False + + - Return True +- ... + +4.2 Customer claim +------------------ + +We present here the customer claim transaction ``cust-claim-tx`` following a ``close-tx`` transaction. + +============= =============================== +Field Value +============= =============================== +... ... +tx_in_count 0 +tx_in [] +tx_out_count 1 +tx_out [<cust-claim] +tze_in_count 1 +tze_in [<close-in>] +tze_out_count 0 +tze_out [] +lock_time 0 +... ... +============================================= + +With, + +- <close-in>: + + - prevout_hash = <close-tx> + - prevout_in = 0 + - witness: + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 1 // *close mode* + - tze_data_payload_len = + - tze_data_payload = <cust-sig> + + - <cust-claim> being a transparent address comprising an amount of <cust-balance>. + + +The tze_verify function works as follow: + +**tze_verify(type, mode, precondition, witness)** + +- ... +- if mode == 1: + + - // *Check the transaction's structure* + - if context.tx.tx_in_count != 0 OR context.tx.tx_out_count == 1 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0: + + - Return false + + - // *Define the variables* + - cid, merch-pk, cust-pk, rev-lock = precondition + - sig = witness + - msg = TODO // *hash of tx without witnesses* + + - // *Check the tx signature* + - if Verify(cust-pk, msg, sig) == 1: + + + - // *Check the delay* + - close-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO + - if context.block-height < close-height + DELAY: + + - Return False + + - Return True + +- ... + +4.3 Dispute +-------------------- + +We present here how the merchant dispute the transaction ``close-tx`` with a ``dispute-tx`` transaction. + +============= =============================== +Field Value +============= =============================== +... ... +tx_in_count 0 +tx_in [] +tx_out_count 1 +tx_out [<merch-disp] +tze_in_count 1 +tze_in [<close-in>] +tze_out_count 0 +tze_out [] +lock_time 0 +... ... +============================================= + +With, + +- <close-in>: + + - prevout_hash = <close-tx> + - prevout_in = 0 + - witness: + + - tze_id = 0 // *zkChannels' TZE type* + - tze_mode = 1 // *close mode* + - tze_data_payload_len = + - tze_data_payload = <merch-sig> || <rev-secret> + + - <merch-disp> being a transparent address comprising an amount of <cust-balance>. + + +The tze_function works as follow: + +**tze_verify(zkChannels-type, close-mode, precondition, witness)** + +- ... +- if mode == 1: + + - // *Check the transaction's structure* + - if context.tx.tx_in_count != 0 OR context.tx.tx_out_count == 1 OR context.tx.tze_in_count != 1 OR context.tx.tze_out_count != 0 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0: + + - Return false + + - // *Define the variables* + - cid, merch-pk, cust-pk, rev-lock = precondition + - sig = witness + - msg = TODO // hash of tx without witnesses + + - // *Check the tx signature* + - if Verify(merch-pk, msg, sig) == 1: + + - // *Check the hash lock* + - if rev-lock != SHA256(rev-secret): + + - Return False + + - Return True + +- ... + + +TZE_VERIFY function +=================== + + +**tze_verify(mode, precondition, witness):** + +- // *Check the transaction structure* +- if context.tx.tx_in_count != 0 OR context.tx.tze_in_count != 1 OR context.tx.nShieldedSpend !=0 OR context.tx.nShieldedOutput != 0 OR context.tx.nJoinsplit != 0: + + - Return False + +- msg = TODO // hash of tx without witnesses + +- if mode == 0: // *escrow mode* + + - init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition + + - if context.tx.tze_out_count == 1: + + - if context.tx.tze_out[0].precondition.tze_mode == 1: // *This is a close transaction* + + - // *Check the transaction's structure* + - if context.tx.tx_out_count != 1: + + - Return False + + - merch-ps-sig || cust-sig = witness + - cid || merch-pk || cust-pk || rev-lock = context.tx.tze_out[0].precondition.tze_data_payload + - merch-balance = context.tx.tx_out[0].value + - cust-balance = context.tx.tze_out[0].amount + - state = cid || cust-pk || rev-lock || cust-balance || merch-balance + + - if init-cid != cid OR init-merch-pk != merch-pk OR init-cust-pk != cust-pk: + + - Return False + + - if Verify(cust-pk, msg, cust-sig) != 1 OR PS_sig.Verify(merch-ps-pk, state, merch-ps-sig) != 1: + + - Return False + + - if context.tx.tx_out[0] == merch-balance || <ZCash P2PKH version bytes (0x1C || 0xB8 for mainnet)> || Hash_160(merch-pk) + + - Return True + + - if context.tx.tze_out[0].precondition.tze_mode == 2: // *This is an expiry transaction* + + - // *Check the transaction's structure* + - if context.tx.tx_out_count != 0: + + - Return False + + - merch-sig = witness + - cid || merch-pk || merch-ps-pk || cust-pk = context.tx.tze_out[0].precondition.tze_data_payload + + - if init-cid != cid OR init-merch-pk != merch-pk OR init-merch-ps-pk != merch-ps-pk OR init-cust-pk != cust-pk: + + - Return False + + - if Verify(merch-pk, msg, merch-sig) == 1: + + - Return True + + - if context.tx.tze_out_count == 0 AND if context.tx.tx_out_count == 2: // *This is a mutual close transaction* + + - merch-sig || cust-sig = witness + + - if Verify(init-merch-pk, msg, merch-sig) == 1 AND Verify(init-cust-pk, msg, cust-sig) == 1: + + - Return True + + +- else if mode == 1: // *close mode* + + - cid, merch-pk, cust-pk, rev-lock = precondition + - sig, (rev-secret) = witness + + - if context.tx.tx_out.count == 1: + + - // *Check the transaction's structure* + - if context.tx.tze_out_count != 0: + + - Return False + + - if Verify(cust-pk, msg, sig) == 1: // *This is a customer claim transaction* + + - expiry-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO + - if context.block-height >= expiry-height + DELAY: + + - Return True + + + - if Verify(merch-pk, msg, sig) == 1:// *This is a dispute transaction* + + - if rev-lock == SHA256(rev-secret): + + - Return True + + + +- else if mode == 2: // *expiry mode* + + - init-cid || init-merch-pk || init-merch-ps-pk || init-cust-pk = precondition + + - if context.tx.tze_out_count == 1: // *This is a close transaction* + + - // *Check the transaction's structure* + - if context.tx.tx_out_count != 1: + + - Return False + + - merch-ps-sig || cust-sig = witness + - cid || merch-pk || cust-pk || rev-lock = context.tx.tze_out[0].precondition.tze_data_payload + - merch-balance = context.tx.tx_out[0].value + - cust-balance = context.tx.tze_out[0].amount + - state = cid || cust-pk || rev-lock || cust-balance || merch-balance + + - if init-cid != cid OR init-merch-pk != merch-pk OR init-cust-pk != cust-pk: + + - Return False + + - if Verify(cust-pk, msg, cust-sig) != 1 OR PS_sig.Verify(merch-ps-pk, state, merch-ps-sig) != 1: + + - Return False + + - if context.tx.tx_out[0] == merch-balance || <ZCash P2PKH version bytes (0x1C || 0xB8 for mainnet)> || Hash_160(merch-pk) + + - Return True + + - else if context.tx.tx_out_count == 1: // *This is a merchant claim transaction* + + - sig = witness + + - if Verify(merch-pk, msg, sig) == 1: + + - expiry-height = find_block_height(context.tx.tze_input[0].prevout_hash) // TODO + - if context.block-height >= expiry-height + DELAY: + + - Return True + + +- Return False + +Transaction Malleability +======================== + +As the transaction id is not built upon the TZE witnesses, any change to the transaction's signature does not modify its id. + +The preconditions are non-malleable as they are signed with a non-malleable signature. + +To prevent witness malleability, we intend to fix the witness payload's size in the tze_verify such that the addition or removal of any bit would result in a verification failure. Any change to the bits should either result in a verification failure, when the revocation secret or a signature is corrupted, or a pass, when the PS signature is randomized. + +References +========== +.. [#BoltTZE] _`Bolt TZE implementation for Zcash <https://github.com/boltlabs-inc/librustzcash>` +.. [#RFC2119] `Key words for use in RFCs to Indicate Requirement Levels <https://tools.ietf.org/html/rfc2119>`_ +.. [#bolt-paper] `Bolt: Anonymous Payment Channels for Decentralized Currencies <https://eprint.iacr.org/2016/701>`_ +.. [#zip-0222] `ZIP 222: Transparent Zcash Extensions (Draft) <https://github.com/zcash/zips/pull/248>`_ \ No newline at end of file
zip-bolt-support.rst +334 −0
@@ -0,0 +1,334 @@ +:: + + ZIP: XXX + Title: Add support for Blind Off-chain Lightweight Transactions (Bolt) protocol + Authors: J. Ayo Akinyele <ayo@boltlabs.io> + Colleen Swanson <swan@boltlabs.io> + Credits: Ian Miers <imiers@z.cash> + Matthew Green <mgreen@z.cash> + Category: Consensus + Created: 2019-07-15 + License: MIT + + +Terminology +=========== + +The key words "MUST" and "MUST NOT" in this document are to be interpreted as described in RFC 2119. [#RFC2119]_ + +Abstract +======== + +This proposal specifies three possible approaches for integrating the blind off-chain lightweight transaction (Bolt) protocol [#bolt-paper]_ into Zcash. + +Motivation +========== + +Layer 2 protocols like Lightning enable scalable payments for Bitcoin but lack the mechanisms to provide strong privacy guarantees on the payment network. Zcash offers private transactions but currently lacks features that would enable a lightning-style payment channel. This proposal specifies an integration of the Bolt privacy-preserving Lightning protocol on top of Zcash [#bolt-paper]_. + +Specification +============= + +This specification details an initial approach to integrating the features of Bolt into Zcash in a future network upgrade and depends on the WTP ZIP [#wtp-programs]_ that introduces Whitelisted Transparent Programs (WTPs). Our prototype implementation can be found here: [#BoltWTP]_. + +1. General requirements for Bolt protocol +-------------------------- + +Bolt private payment channels require the following capabilities to provide anonymity properties for users on a payment network: + +(1) Ability to create a escrow transaction such that the transaction inputs are anonymous. +(2) Ability to escrow funds to a multi-signature style address via non-malleable transactions. +(3) Ability to specify relative time locks for commitment transactions to support unilateral channel closing. +(4) Ability to specify absolute and relative time locks to support HTLCs for multi-hop payments. +(5) Ability to validate Bolt-specific opening and closing transactions: + + - check the validity of randomized/blinded signature on the wallet commitment in closing token + - check the validity of revocation token in the event of a channel dispute by merchant + +(6) Ability to verify transaction outputs using WTPs such that: + + - if customer-initiated closing, one output pays out to customer with a time lock (to allow merchant to dispute customer balance) and one output pays out to merchant immediately + - if merchant-initiated closing, a single output pays the merchant the full balance of the channel with a time lock that allows for customer dispute + +**Channel Operation Assumptions.** + - Channels funded by the customer alone and dual-funded channels are both supported. + - Either the customer or the merchant can initiate channel closing. + - If the customer initiates closing, then the merchant can dispute the closing transaction if they disagrees with the closing token in the closing transaction. + - If the merchant initiates closing, the merchant posts a transaction claiming all the funds in the channel for themselves with a timelock. This gives the customer the opportunity to post their own valid closing transaction with the current channel balances. If the customer posts their own closing transaction, the merchant has an additional opportunity to dispute if necessary. + +1.1 Customer and Merchant Signing Keys +------------- + +The customer and the merchant both have key pairs from a suitable signature scheme. These are denoted as: +``<cust-pk>, <cust-sk>`` and +``<merch-pk>, <merch-sk>``, respectively, where ``pk`` stands for "public key" and ``sk`` stands for the corresponding "secret key". + +The merchant must be able to issue blind signatures, so they have an additional keypair; this keypair is denoted as: +``<MERCH-PK>, <MERCH-SK>``. + +The customer key pair is specific to the channel and must not be reused. The merchant key pair is long term and should be used for all customer channels. + +1.2 Wallets +------------- +A Bolt channel allows a customer to make or receive a sequence of payments off chain. These payments are tracked and validated using a sequence of *wallets*. A wallet consists of the customer's public key (which ties the wallet to the channel), a wallet-specific public key (which can be from any suitable signature scheme), denoted ``<wpk>``, and the current customer and merchant balances. + +After each payment, the customer receives an updated wallet and blind signatures from the merchant on the wallet contents allowing channel close as specified below. + +1.2 Opening a Channel: Overview +------------- +To open a channel, the customer and merchant exchange key information and set the channel token ``<channel-token> = <cust-pk>, <merch-pk>, <MERCH-PK>``. + +They agree on their respective initial balances ``initial-cust-balance`` and ``initial-merch-balance``. + +The customer picks an inital wallet public key ``<wpk>``. + +The customer and merchant escrow the necessary funds in a funding transaction, denoted ``escrow-tx``. + +1.3 Closing a Channel: Overview +------------- + +A customer should be able to close the channel by posting a *closing token* ``close-token``, which is a blind signature from the merchant under ``<MERCH-PK>`` on a special closing wallet that contains ``<cust-pk>, <wpk>, <balance-cust>, <balance-merch>, CLOSE``. We use ``cust-close-tx`` to denote the transaction posted by the customer to initiate channel closure. + +A merchant should be able to close the channel by either posting a special closing transaction ``merch-close-tx`` (detailed in Section 2.3.2) or, if the customer posts an outdated version of their closing token, a signed revocation token, ``revocation-token`` as detailed below. The revocation token ``revocation-token`` is a signature under the wallet public key ``<wpk>`` on the special revocation message ``<wpk> || REVOKED``. The transaction posted by the merchant to dispute is denoted ``dispute-tx``. + +The customer and merchant may also negotiate off-chain to form a *mutual close transaction*, ``mutual-close-tx``. Off-chain collaboration to create ``mutual-close-tx`` reduces the required number of on-chain transactions and eliminates the time delays. + +2. Transparent/Shielded Tx: Using T/Z-addresses and WTPs +------------- + +We assume the following specific features are present: + +(1) Support for whitelisted transparent programs (WTPs) that enables 2-of-2 multi-sig style transactions +(2) Can specify absolute lock time in transaction +(3) Can specify relative lock time in transparent program +(4) Can specify shielded inputs and outputs +(5) A non-SegWit approach that fixes transaction malleability +(6) ``OP_BOLT`` logic expressed as WTPs. We will use the Bolt WTPs defined in Section 2.1: ``open-channel``, ``cust-close``, and ``merch-close``. + +**Privacy Limitations**. The aggregate balance of the channel will be revealed in the funding transaction ``escrow-tx``. Similarly, the final splitting of funds will be revealed to the network. However, for channel opening and closing, the identity of the participants remains hidden. Channel opening and closing will also be distinguishable on the network due to use of WTPs. + +**Channel Opening**. The funding transaction ``escrow-tx`` spends ZEC from one or more shielded addresses to a transparent output that is encumbered by a Bolt transparent program. See Section 2.1 for what the funding transaction looks like when instantiated using WTPs. + +2.1 Bolt WTPs +-------------- + +Transparent programs take as input a ``predicate``, ``witness``, and ``context`` and then output a ``True`` or ``False`` on the stack. Bolt-specific transparent programs are deterministic and any malleation of the ``witness`` will result in a ``False`` output. The WTPs are as follows: + +1. ``open-channel`` program. The purpose of this WTP is to encumber the funding transaction such that either party may initiate channel closing as detailed above in Section 1.3. The program is structured as follows: + + a. ``predicate``: The predicate consists of ``<<channel-token> || <merch-close-address>>``, where ``<channel-token> = <<cust-pk> || <merch-pk> || <MERCH-PK>>`` contains three public keys, one for the customer and two for the merchant, and an address ``<merch-close-address>`` for the merchant at which to receive funds from a customer-initiated close. + + b. ``witness``: The witness is defined as follows, where the first byte is used to denote witness type: + + 1. ``<<0x0> || <balance-cust> || <balance-merch> || <cust-sig> || <merch-sig>>`` + 2. ``<<0x1> || <balance-cust> || <balance-merch> || <cust-sig> || <wpk> || <closing-token>>`` + + c. ``verify_program`` behaves as follows: + + 1. If witness is of type ``0x0``, check that 2 new outputs are created, with the specified balances (unless one of the balances is zero), and that the signatures verify. + 2. If witness is of type ``0x1``, check that 2 new outputs are created (unless one of the balances is zero), with the specified balances: + + + one paying ``<balance-merch>`` to ``<merch-close-address>`` + + one paying a ``cust-close`` WTP containing ``<channel-token>`` and ``<wallet> = <<wpk> || <balance-cust> || <balance-merch>>`` + + Also check that ``<cust-sig>`` is a valid signature and that ``<closing-token>`` contains a valid signature under ``<MERCH-PK>`` on ``<<cust-pk> || <wpk> || <balance-cust> || <balance-merch> || CLOSE>``. + +2. ``cust-close`` program. The purpose of this WTP is to allow the customer to initiate channel closure as specified in Section 1.3. The program is specified as follows: + + a. ``predicate``: ``<<channel-token> || <block-height> || <wallet>>``, where + + 1. ``<channel-token> = <<cust-pk> || <merch-pk> || <MERCH-PK>>``, + 2. ``<block_height>`` is the earliest block-height when balance can be spend, and + 3. ``<wallet> = <<wpk> || <balance-cust> || <balance-merch>>``. + b. ``witness``: The witness is defined as one of the following, where the first byte is used to denote witness type: + + 1. ``<<0x0> || <cust-sig>>`` + 2. ``<<0x1> || <merch-sig> || <address> || <revocation-token>>`` + c. ``verify_program`` behaves as follows: + + 1. If witness is of type ``0x0``, check that ``<cust-sig>`` is valid and ``<block-height>`` has been reached + 2. If witness is of type ``0x1``, check that 1 output is created paying ``<balance-cust>`` to ``<address>``. Also check that ``<merch-sig>`` is a valid signature on ``<<address> || <revocation-token>>`` and that ``<revocation-token>`` contains a valid signature under ``<wpk>`` on ``<<wpk> || REVOKED>``. + +3. ``merch-close``. The purpose of this WTP is to allow a merchant to initiate channel closure as specified in Section 1.3. The program is specified as follows: + + a. ``predicate``: ``<<channel-token> || <block-height> || <merch-close-address>>``. + b. ``witness`` is defined as one of the following, where the first byte is used to denote witness type: + + 1. ``<<0x0> || <merch-sig>>`` + 2. ``<<0x1> || <cust-sig> || <wallet> || <closing-token>>``, where ``<wallet> = <<wpk> || <balance-cust> || <balance-merch>>``. + c. ``verify_program`` behaves as follows: + + 1. If witness is of type ``0x0``, check that ``<merch-sig>`` is valid and ``<block-height>`` has been reached + 2. If witness is of type ``0x1``, check that 2 new outputs are created (unless one of the balances is zero), with the specified balances: + + + one paying ``<balance-merch>`` to ``<merch-close-address>`` + + one paying a ``cust_close`` WTP containing ``<wallet> = <<wpk> || <balance-cust> || <balance-merch>>`` and ``<channel-token>``. + + Also check that ``<cust-sig>`` is a valid signature and that ``<closing-token>`` contains a valid signature under ``<MERCH-PK>`` on ``<<cust-pk> || <wpk> || <balance-cust> || <balance-merch> || CLOSE>``. + + +2.2 Channel establishment and Funding Transaction +------------- +The funding transaction ``escrow-tx`` by default has 2 shielded inputs (but can be up to some N) and an ``open-channel`` WTP output with predicate ``<<channel-token> <merch-close-address>>``. + +* ``lock_time``: 0 +* ``nExpiryHeight``: 0 +* ``valueBalance``: funding amount + transaction fee +* ``nShieldedSpend``: 1 or N (if funded by both customer and merchant) +* ``vShieldedSpend[0]``: tx for customer’s note commitment and nullifier for the coins + + - ``cv``: commitment for the input note + - ``root``: root hash of note commitment tree at some block height + - ``nullifier``: unique serial number of the input note + - ``rk``: randomized pubkey for spendAuthSig + - ``zkproof``: zero-knowledge proof for the note + - ``spendAuthSig``: signature authorizing the spend + +* ``vShieldedSpend[1..N]``: additional tx for customer's note commitment and nullifier for the coins + + - ``cv``: commitment for the input note + - ``root``: root hash of note commitment tree at some block height + - ``nullifier``: unique serial number of the input note + - ``rk``: randomized pubkey for spendAuthSig + - ``zkproof``: zero-knowledge proof for the note + - ``spendAuthSig``: signature authorizing the spend +* ``tx_out_count``: 1 +* ``tx_out``: (via a transparent program) + + - ``scriptPubKey``: ``PROGRAM PUSHDATA( <open-channel> || <<channel-token> || <merch-close-address>> )`` + +* ``bindingSig``: a signature that proves that (1) the total value spent by Spend transfers - Output transfers = value balance field. + +The customer and merchant collaborate to create the customer's initial closing token ``closing-token`` and the merchant closing transaction ``merch-close-tx`` before signing and sending ``escrow-tx`` to the network. Once the transaction has been confirmed, the payment channel is established. + +2.3 Channel Closing +------------- +2.3.1 Customer-initiated channel closing. +---- +To initiated channel closure, a customer posts the transaction ``cust-close-tx`` that spends from ``escrow-tx`` and contains two outputs: (1) an output that can be spent immediately by the merchant and (2) a ``cust-close`` WTP output that can be spent either by the customer after a relative timeout or by the merchant with a revocation token. This approach allows the merchant to dispute if the customer posts a transaction containing a spent closing token (i.e., a closing token that is valid from the network's perspective but outdated from the merchant's perspective). + +The transaction ``cust-close-tx`` is as follows: + +* ``version``: specify version number +* ``groupid``: specify group id +* ``locktime``: should be set such that closing transactions can be included in a current block. +* ``txin`` count: 1 + + - ``txin[0]`` outpoint: references the funding transaction txid and output_index + - ``txin[0]`` script bytes: 0 + - ``txin[0]`` scriptSig: ``PROGRAM PUSHDATA( <open-channel> || <<0x1> || <balance-cust> || <balance-merch> || <cust-sig> || <wpk> || <closing-token>> )`` + +* ``txout`` count: 2 +* ``txouts``: + + * ``to_customer``: a ``cust-close`` WTP output. + + - ``amount``: ``<balance-cust>`` + - ``nSequence: <time-delay>`` + - ``scriptPubKey``: ``PROGRAM PUSHDATA( <cust-close> || <<channel-token> || <wallet>> )`` + + * ``to_merchant``: a P2PKH output sending funds to the merchant, i.e. + + * ``scriptPubKey``: ``0 <20-byte-key-hash of merch-close-address>`` + * ``amount``: ``<balance-merch>`` + * ``nSequence``: 0 + +To redeem the ``to_customer`` output, the customer posts a secondary closing transaction after the appropriate time delay with the following ``scriptSig``: + + ``PROGRAM PUSHDATA( <cust-close> || <<0x0> || <cust-sig> || <block-height>> )`` + +where the ``witness`` consists of a first byte ``0x0`` to indicate the witness type followed by the customer signature and the current block height (used to ensure that timeout reached). + +If the customer posts a spent closing token, the merchant can dispute and redeem the ``to_customer`` output by posting a transaction ``dispute-tx`` that spends from ``cust-close-tx`` with the following ``scriptSig``: + + ``PROGRAM PUSHDATA( <cust-close> || <<0x1> || <merch-sig> || <revocation-token>> )`` + +where the ``witness`` consists of a first byte ``0x1`` to indicate the witness type followed by the merchant signature and the revocation token. + +2.3.2 Merchant-initiated channel closure +---- +To initiate channel closure, the merchant posts the following transaction ``merch-close-tx`` (formed and signed during channel establishment) that spends from ``escrow-tx``: + +* ``version``: specify version number +* ``groupid``: specify group id +* ``locktime``: should be set such that closing transactions can be included in a current block. +* ``txin`` count: 1 + + - ``txin[0]`` outpoint: references the funding transaction txid and output_index + - ``txin[0]`` script bytes: 0 + - ``txin[0]`` scriptSig: ``PROGRAM PUSHDATA( <open-channel> || <<0x0> || <balance-cust> || <balance-merch> || <cust-sig> || <merch-sig>> )`` + +* ``txout`` count: 1 +* ``txouts``: + + * ``to_merchant``: a ``merch-close`` WTP output. + + - ``amount``: sum of ``<balance-cust>`` and ``<balance-merch>`` + - ``nSequence: <time-delay>`` + - ``scriptPubKey``: ``PROGRAM PUSHDATA( <merch-close> || <<channel-token> || <merch-close-address>> )`` + +To spend this output, the merchant posts a secondary closing transaction after the appropriate time delay with the following ``scriptSig``: + + ``PROGRAM PUSHDATA( <merch-close> || <<0x0> || <merch-sig> || <block-height>> )`` + +where the ``witness`` consists of a first byte ``0x0`` to indicate witness type, followed by the merchant signature and the current block height (used to ensure that the timeout has been reached). + +If the customer sees ``merch-close-tx`` on chain, and the current customer balance in the channel is actually non-zero, the customer should post their own closing transaction. This closing transaction is nearly identical to that specified in the customer-initiated channel closure section above and allows for merchant dispute in the same way: + +* ``version``: specify version number +* ``groupid``: specify group id +* ``locktime``: should be set such that closing transactions can be included in a current block. +* ``txin`` count: 1 + + - ``txin[0]`` outpoint: references the ``merch-close-tx`` txid and output_index + - ``txin[0]`` script bytes: 0 + - ``txin[0]`` scriptSig: ``PROGRAM PUSHDATA( <merch-close> || <<0x1> || <balance-cust> || <balance-merch> || <cust-sig> || <wpk> || <closing-token>> )`` + +* ``txout`` count: 2 +* ``txouts``: + + * ``to_customer``: a ``cust-close`` WTP output. + + - ``amount``: ``<balance-cust>`` + - ``nSequence: <time-delay>`` + - ``scriptPubKey``: ``PROGRAM PUSHDATA( <cust-close> || <<channel-token> || <wallet>> )`` + + * ``to_merchant``: a P2PKH output sending funds to the merchant, i.e. + + * ``scriptPubKey``: ``0 <20-byte-key-hash of merch-close-address>`` + * ``amount``: ``<balance-merch>`` + * ``nSequence``: 0 + + +2.3.3 Mutual closing +------------- +The customer and merchant can alternatively collaborate off-chain to create a mutual closing transaction ``mutual-close-tx`` that spends from ``escrow-tx``. This transaction is as follows: + + +* ``version``: specify version number +* ``groupid``: specify group id +* ``locktime``: should be set such that closing transactions can be included in a current block. +* ``txin`` count: 1 + + - ``txin[0]`` outpoint: references the funding transaction txid and output_index + - ``txin[0]`` script bytes: 0 + - ``txin[0]`` scriptSig: ``PROGRAM PUSHDATA( <open-channel> || <<0x0> || <balance-cust> || <balance-merch> || <cust-sig> || <merch-sig>> )`` + +* ``txout`` count: 2 +* ``txouts``: + + * ``to_customer``: an output paying ``<balance-cust>`` + * ``to_merchant``: an output paying ``<balance-merch>`` + + +Reference Implementation +======================== + +.. [#BoltWTP] _`Bolt WTP implementation for Zcash <https://github.com/boltlabs-inc/librustzcash>` + +References +========== + +.. [#RFC2119] `Key words for use in RFCs to Indicate Requirement Levels <https://tools.ietf.org/html/rfc2119>`_ +.. [#bolt-paper] `Bolt: Anonymous Payment Channels for Decentralized Currencies <https://eprint.iacr.org/2016/701>`_ +.. [#wtp-programs] `ZIP XXX: Whitelisted Transparent Programs (Draft) <https://github.com/zcash/zips/pull/248>`_