Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

64bit arithmetic op codes #1538

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
302 changes: 302 additions & 0 deletions bip-0364.mediawiki
@@ -0,0 +1,302 @@
<pre>
BIP: TBD
Layer: Consensus (soft fork)
Title: 64 bit arithmetic operations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

Suggested change
Title: 64 bit arithmetic operations
Title: 64-bit arithmetic operations

Author: Chris Stewart <stewart.chris1234@gmail.com>
Comments-Summary: No comments yet.
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0364
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please refrain from issuing your own BIP numbers.

Status: Draft
Type: Standards Track
Created: 2023-09-11
License: BSD-3-Clause
</pre>

==Abstract==

This BIP describes a new set of arithmetic opcodes (OP_ADD64, OP_SUB64, OP_MUL64, OP_DIV64, OP_NEG64,
OP_LESSTHAN64, OP_LESSTHANOREQUAL64, OP_GREATERTHAN64, OP_GREATERTHANOREQUAL64)
that allows 64 bit signed integer math in the bitcoin protocol.

This BIP also describes a set of conversion opcodes (OP_SCRIPTNUMTOLE64, OP_LE64TOSCRIPTNUM, OP_LE32TOLE64)
to convert existing bitcoin protocol numbers (CScriptNum) into 4 and 7 byte little endian representations.

==Summary==

The arithmetic opcodes (OP_ADD64, OP_SUB64, OP_MUL64, OP_DIV64) behave as follows

* Fail if less than 2 elements on the stack
* Fail if the stacks top 2 elements are not exactly 8 bytes
* If the operation results in an overflow, push false onto the stack
* If the operation succeeds without overflow, push the result and true onto the stack

OP_DIV64
* Fail if less than 2 elements on the stack
* Fail if the stacks top 2 elements are not exactly 8 bytes
* If the stack top is zero (denominator), push false onto the stack
* If the stack top is -1 and the numerator is -9223372036854775808, push false onto the stack
* Calculate the remainder (`r = a % b`) and quotient (`q = a / b`)
* If the remainder is negative, invert it to be positive
* Push the remainder onto the stack
* Push the quotient onto the stack
* Push true onto the stack

64 bit comparison opcodes (OP_LESSTHAN64, OP_LESSTHANOREQUAL64, OP_GREATERTHAN64, OP_GREATERTHANOREQUAL64)
* Fail if less than 2 elements on the stack
* Fail if the stacks top 2 elements are not exactly 8 bytes
* Push the boolean result of the comparison onto the stack

OP_NEG64
* Fail if less than 1 element on the stack
* Fail if the stacks top is not exactly 8 bytes
* If the operation results in an overflow, push false onto the stack
* Push the result of negating the stack top onto the stack and push true onto the stack

OP_SCRIPTNUMTOLE64
* Fail if less than 1 element on the stack
* Interpret the stack top as a CScriptNum
* Push the 8 byte little endian representation of the number onto the stack

OP_LE64TOSCRIPTNUM
* Fail if less than 1 element on the stack
* Fail if the stack top is not exactly 8 bytes
* Interpret the stack top as a 8 byte little endian number
* Fail if the 8 byte little endian number would overflow CScriptNum
* Push the byte representation of CScriptNum onto the stack

OP_LE32TOLE64
* Fail if less than 1 element on the stack
* Fail if the stack top is not exactly 4 bytes in size
* Interpret the stack top as a 32 bit little endian number
* Push the 8 byte little endian number onto the stack


==Motivation==

64 bit arithmetic operations are required to support arithmetic on satoshi values.
Math on satoshis required precision of 51 bits. Many bitcoin protocol proposals - such as covenants -
require Script access to output values. To support the full range of possible output values
we need 64 bit precision.

===OP_INOUT_AMOUNT===

[https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-September/019420.html OP_INOUT_AMOUNT] is
part of the [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-September/019419.html OP_TAPLEAFUPDATE_VERIFY] soft fork proposal.
This opcode pushes two values onto the stack, the amount from this
input's utxo, and the amount in the corresponding output, and then expect
anyone using OP_TLUV to use maths operators to verify that funds are being
appropriately retained in the updated scriptPubKey.

Since the value of the utxos can be up to 51 bits in value, we require 64 bit
arithmetic operations.


==Overflows==

When dealing with overflows, we explicitly return the success bit as a CScriptNum at the top of the stack and the result being the second element from the top. If the operation overflows, first the operands are pushed onto the stack followed by success bit. [a_second a_top] overflows, the stack state after the operation is [a_second a_top 0] and if the operation does not overflow, the stack state is [res 1].

This gives the user flexibility to deal if they script to have overflows using OP_IF\OP_ELSE or OP_VERIFY the success bit if they expect that operation would never fail. When defining the opcodes which can fail, we only define the success path, and assume the overflow behavior as stated above.

==Detailed Specification==

Refer to the reference implementation, reproduced below, for the precise
semantics and detailed rationale for those semantics.

<source lang="cpp">


static inline int64_t cast_signed64(uint64_t v)
{
uint64_t int64_min = static_cast<uint64_t>(std::numeric_limits<int64_t>::min());
if (v >= int64_min)
return static_cast<int64_t>(v - int64_min) + std::numeric_limits<int64_t>::min();
return static_cast<int64_t>(v);
}

static inline int64_t read_le8_signed(const unsigned char* ptr)
{
return cast_signed64(ReadLE64(ptr));
}

static inline void push8_le(std::vector<valtype>& stack, uint64_t v)
{
uint64_t v_le = htole64(v);
stack.emplace_back(reinterpret_cast<unsigned char*>(&v_le), reinterpret_cast<unsigned char*>(&v_le) + sizeof(v_le));
}

case OP_ADD64:
case OP_SUB64:
case OP_MUL64:
case OP_DIV64:
case OP_LESSTHAN64:
case OP_LESSTHANOREQUAL64:
case OP_GREATERTHAN64:
case OP_GREATERTHANOREQUAL64:
{
// Opcodes only available post tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

if (stack.size() < 2)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

valtype& vcha = stacktop(-2);
valtype& vchb = stacktop(-1);
if (vchb.size() != 8 || vcha.size() != 8)
return set_error(serror, SCRIPT_ERR_EXPECTED_8BYTES);

int64_t b = read_le8_signed(vchb.data());
int64_t a = read_le8_signed(vcha.data());

switch(opcode)
{
case OP_ADD64:
if ((a > 0 && b > std::numeric_limits<int64_t>::max() - a) ||
(a < 0 && b < std::numeric_limits<int64_t>::min() - a))
stack.push_back(vchFalse);
else {
popstack(stack);
popstack(stack);
push8_le(stack, a + b);
stack.push_back(vchTrue);
}
break;
case OP_SUB64:
if ((b > 0 && a < std::numeric_limits<int64_t>::min() + b) ||
(b < 0 && a > std::numeric_limits<int64_t>::max() + b))
stack.push_back(vchFalse);
else {
popstack(stack);
popstack(stack);
push8_le(stack, a - b);
stack.push_back(vchTrue);
}
break;
case OP_MUL64:
if ((a > 0 && b > 0 && a > std::numeric_limits<int64_t>::max() / b) ||
(a > 0 && b < 0 && b < std::numeric_limits<int64_t>::min() / a) ||
(a < 0 && b > 0 && a < std::numeric_limits<int64_t>::min() / b) ||
(a < 0 && b < 0 && b < std::numeric_limits<int64_t>::max() / a))
stack.push_back(vchFalse);
else {
popstack(stack);
popstack(stack);
push8_le(stack, a * b);
stack.push_back(vchTrue);
}
break;
case OP_DIV64:
{
if (b == 0 || (b == -1 && a == std::numeric_limits<int64_t>::min())) { stack.push_back(vchFalse); break; }
int64_t r = a % b;
int64_t q = a / b;
if (r < 0 && b > 0) { r += b; q-=1;} // ensures that 0<=r<|b|
else if (r < 0 && b < 0) { r -= b; q+=1;} // ensures that 0<=r<|b|
popstack(stack);
popstack(stack);
push8_le(stack, r);
push8_le(stack, q);
stack.push_back(vchTrue);
}
break;
break;
case OP_LESSTHAN64: popstack(stack); popstack(stack); stack.push_back( (a < b) ? vchTrue : vchFalse ); break;
case OP_LESSTHANOREQUAL64: popstack(stack); popstack(stack); stack.push_back( (a <= b) ? vchTrue : vchFalse ); break;
case OP_GREATERTHAN64: popstack(stack); popstack(stack); stack.push_back( (a > b) ? vchTrue : vchFalse ); break;
case OP_GREATERTHANOREQUAL64: popstack(stack); popstack(stack); stack.push_back( (a >= b) ? vchTrue : vchFalse ); break;
default: assert(!"invalid opcode"); break;
}
}
break;
case OP_NEG64:
{
// Opcodes only available post tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

if (stack.size() < 1)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

valtype& vcha = stacktop(-1);
if (vcha.size() != 8)
return set_error(serror, SCRIPT_ERR_EXPECTED_8BYTES);

int64_t a = read_le8_signed(vcha.data());
if (a == std::numeric_limits<int64_t>::min()) { stack.push_back(vchFalse); break; }

popstack(stack);
push8_le(stack, -a);
stack.push_back(vchTrue);
}
break;

case OP_SCRIPTNUMTOLE64:
{
// Opcodes only available post tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

if (stack.size() < 1)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

int64_t num = CScriptNum(stacktop(-1), fRequireMinimal).getint();
popstack(stack);
push8_le(stack, num);
}
break;
case OP_LE64TOSCRIPTNUM:
{
// Opcodes only available post tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

if (stack.size() < 1)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

valtype& vchnum = stacktop(-1);
if (vchnum.size() != 8)
return set_error(serror, SCRIPT_ERR_EXPECTED_8BYTES);
valtype vchscript_num = CScriptNum(read_le8_signed(vchnum.data())).getvch();
if (vchscript_num.size() > CScriptNum::nDefaultMaxNumSize) {
return set_error(serror, SCRIPT_ERR_ARITHMETIC64);
} else {
popstack(stack);
stack.push_back(std::move(vchscript_num));
}
}
break;
case OP_LE32TOLE64:
{
// Opcodes only available post tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

if (stack.size() < 1)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

valtype& vchnum = stacktop(-1);
if (vchnum.size() != 4)
return set_error(serror, SCRIPT_ERR_ARITHMETIC64);
uint32_t num = ReadLE32(vchnum.data());
popstack(stack);
push8_le(stack, static_cast<int64_t>(num));
}
break;
</source>

https://github.com/Christewart/bitcoin/commits/64bit-arith

==Deployment==

todo

==Credits==

This work is borrowed from work done on the elements project, with implementations done by Sanket Kanjalkar and Andrew Poelstra.

https://github.com/ElementsProject/elements/pull/1020/files

==References==

https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-September/019419.html

https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-September/019420.html

==Copyright==

This document is placed in the public domain.