Skip to content

Instantly share code, notes, and snippets.

@moonsettler
Last active October 30, 2024 13:36
Show Gist options
  • Save moonsettler/d7f1fb88e3e54ee7ecb6d69ff126433b to your computer and use it in GitHub Desktop.
Save moonsettler/d7f1fb88e3e54ee7ecb6d69ff126433b to your computer and use it in GitHub Desktop.
OP_PAIRCOMMIT (wip)
BIN-2024-000? OP_PAIRCOMMIT
Revision 004 (2024-10-28)
Author moonsettler <moonsettler@protonmail.com>
Layer Consensus (soft fork)
Status Draft
License BSD-3-CLAUSE
Discussion [https://delvingbitcoin.org/?]
Aliases [BIPs PR#????(https://github.com/bitcoin/bips/pull/?)]

Abstract

This BIP describes a new tapscript opcode OP_PAIRCOMMIT which provide limited vector committment functionality.

When evaluated, the OP_PAIRCOMMIT instruction:

  • Pops the top two values off the stack,
  • takes the "PairCommit" tagged SHA256 hash of the stack elements,
  • pushes the resulting committment on the top of the stack.

Specification

OP_PAIRCOMMIT pops two elements off the stack, then concatenates them along with their size committments and takes the tagged SHA256 hash of that concatenated string, then pushes the resulting hash back on the stack.

Given the stack [x1, x2], where x2 is at the top of the stack:

OP_PAIRCOMMIT will push SHA256(tagPC|x1|x2|len(x1)|pad|len(x2)|pad) onto the stack.

Where | denotes concatenation and tagPC is SHA256("PairCommit")|SHA256("PairCommit") and pad is hex 00|00|00|01.

Implementation

case OP_PAIRCOMMIT: {
    // OP_PAIRCOMMIT is only available in Tapscript
    if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
    if (flags & SCRIPT_VERIFY_DISCOURAGE_PAIRCOMMIT) {
        return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS);
    }

    // x1 x2 -- hash
    if (stack.size() < 2) {
        return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
    }
    const valtype& vch1 = stacktop(-2);
    const valtype& vch2 = stacktop(-1);

    uint256 hash = PairCommitHash(vch1, vch2);

    popstack(stack);
    popstack(stack);
    stack.push_back(ToByteVector(hash));
    break;
}

Serialization happens with little endian byte order as per default

const HashWriter HASHER_PAIRCOMMIT{TaggedHash("PairCommit")};

uint256 PairCommitHash(Span<const unsigned char> x1, Span<const unsigned char> x2)
{
    // PAD is 0x00, 0x00, 0x00, 0x01 in little endian serializaton
    static const uint32_t PCPAD = 0x01000000u;

    HashWriter ss{HASHER_PAIRCOMMIT};
    ss << x1
       << x2
       << uint32_t(x1.size()) << PCPAD
       << uint32_t(x2.size()) << PCPAD;

    return ss.GetSHA256();
}

See more: https://github.com/lnhance/bitcoin/pull/6/files

Motivation

Pair or vector committments are generally useful in a covenant script toolkit. Finding the simplest and safest way to do such data committments, and making sure to the outmost degree possible, that it would not intoduce unintended behavior, such as novel 2-way peg mechanisms was the primary motivation for this proposal.

The number of SHA256 iterations is minimized in the most likely use case we can optimize for, which is LN-Symmetry. Since the Tag can be pre-computed as mid-state, it would only take 1 or 2 hash cycles in validation for the unilateral close scenario for Symmetry channels.

In case of a 7 byte balance commitment + 32 byte CTV hash (no HTLCs in-flight), the total preimage size is 55 bytes. Which should make it fit into a single block with the SHA256 length commitment.

In case there are 2x 32 byte CTV hash commitments, the first 64 byte block is comprised of those hashes, and the second block is the vectors’ size and total length commitment, which would be a largely 0 filled block with a very few bits set to 1.

It’s a particular concern for LN-Symmetry with CTV that the concatenation of the two preimages allows for length redistribution attacks, because CTV is only defined for 32 byte templates and will act as NOP for different template sizes for upgradeability.

Vector Commitments

OP_PAIRCOMMIT can be used to commit to a vector of stack elements in a way that is not vulnerable to various forms of witness malleability especially when used in conjunction with OP_CHECKSIGFROMSTACK1 and OP_INTERNALKEY2, making the script cleaner, and simpler. If OP_CAT3 was used naively, the contract could be easily broken since OP_CHECKTEMPLATEVERIFY4 is only defined for 32 byte parameters.

# S = 500000000
# IK -> A+B
<sig> <state-n-recovery-data> <state-n-hash> | CTV PC IK CSFS <S+1> CLTV DROP

before funding sign first state template:

# state-n-hash { nLockTime(S+n), out(contract, amount(A)+amount(B)) }
# settlement-n-hash { nSequence(2w), out(A, amount(A)), out(B, amount(B)) }
# state-n-recovery-data { settlement-n-hash or state-n-balance }

# contract for state n < m
IF
  <sig> <state-m-recovery-data> <state-m-hash> | CTV PC IK CSFS <S+n+1> CLTV DROP
ELSE
  <settlement-n-hash> CTV
ENDIF

Reference Implementation

A reference implementation is provided here:

https://github.com/?

Backward Compatibility

By constraining the behavior of OP_SUCCESS opcodes, deployment of the BIP can be done in a backwards compatible, soft-fork manner. If anyone were to rely on the OP_SUCCESS behavior of OP_SUCCESS205, OP_PAIRCOMMIT would invalidate their spend.

Deployment

TBD

Credits

Jeremy Rubin, Brandon Black, Salvatore Ingala

Copyright

This document is licensed under the 3-clause BSD license.

Footnotes

  1. OP_CHECKSIGFROMSTACK, "BIN-2024-0003"

  2. OP_INTERNALKEY, "BIN-2024-0004"

  3. OP_CAT, "BIN-2024-0001"

  4. OP_CHECKTEMPLATEVERIFY, "BIP 119"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment