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

BIP draft: 64-bit arithmetic opcodes #1538

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
336 changes: 336 additions & 0 deletions bip-0364.mediawiki
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
<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 <[email protected]>
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 re-enables two opcodes: OP_MUL and OP_DIV.

This BIP also expands supported precision for valid operands from `-2^31 +1` to `2^31 -1` to `-2^63 +1...2^63 -1`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I want to make sure I understand, this isn't proposing new opcodes that handle 64-bit arithmetic is it redefining the existing opcodes. Is that correct? It's probably on your TODO list, but what is the mechanism you are using to make this a softfork? Script versioning?

In the future we may wish to increase the supported precision again. What about adding a precision commitment to make soft-forking precision upgrades easier? For instance OP_PUSH x, OP_PRECIS, if x >64 then P_PUSH x OP_PRECIS is an OP_SUCCESSx. Then if we soft precision to 128 bits, OP_PUSH x, OP_PRECIS becomes a NOP.

Copy link
Contributor Author

@Christewart Christewart Feb 3, 2025

Choose a reason for hiding this comment

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

Hi Ethan thanks for your continued interest in this topic.

I want to make sure I understand, this isn't proposing new opcodes that handle 64-bit arithmetic is it redefining the existing opcodes. Is that correct?

👍

It's probably on your TODO list, but what is the mechanism you are using to make this a softfork? Script versioning?

My latest thinking for the upgrade mechanism is allocating a new tap leaf version to indicate the precision of arithmetic operations for Script. This was suggested by @sipa here. You can read about how this works on delving bitcoin and see my implementation of it here inside the bitcoin c++ code base.

I personally think this makes Scripts easier to reason about as the leaf version indicates the precision of all numeric operations during this Script's execution. If we want to increase precision again in the future, we can allocate a new leaf version and extend the underlying precision of CScriptNum and expose that functionality as I've done here

https://github.com/Christewart/bitcoin/blob/20fe67b5e4f2db7326ba5b8771c4516a7010861e/src/script/interpreter.cpp#L257

In the future we may wish to increase the supported precision again. What about adding a precision commitment to make soft-forking precision upgrades easier?

Committing to arbitrary precision seems hard, but i think this BIP lays the groundwork for extending precision in the future by making the precision of arithmetic operations linked with new tapleaf versions.

I pushed to this branch staged changes in a haphazard state, apologies. I'm working on a BIP currently to disallow 64 byte transactions in the bitcoin blockchain as I think that is more likely to be widely accepted by the community, and then will be coming back to this work.


==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 covenant proposals -
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==

This propsal retains overflow semantics from the original bitcoin implementation.

Check warning on line 42 in bip-0364.mediawiki

View workflow job for this annotation

GitHub Actions / Typo Checks

"propsal" should be "proposal".

Results from 64bit numeric opcodes may overflow and are valid as long as they are not used in a subsequent numeric operation.

If overflowed results are used in a subsequent numeric operation, the Script terminates immediately.

==Detailed Specification==

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

<source lang="cpp">
class CScriptNum
Copy link
Contributor

Choose a reason for hiding this comment

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

Big thumbs down at the idea of just pasting a blob of C++ code vs actually specifying the semantics of the new op codes.

{
/**
* Numeric opcodes (OP_1ADD, etc) are restricted to operating on 4-byte integers.
* The semantics are subtle, though: operands must be in the range [-2^31 +1...2^31 -1],
* but results may overflow (and are valid as long as they are not used in a subsequent
* numeric operation). CScriptNum enforces those semantics by storing results as
* an int64 and allowing out-of-range values to be returned as a vector of bytes but
* throwing an exception if arithmetic is done or the result is interpreted as an integer.
*/
public:

explicit CScriptNum(const __int128_t& n)
{
m_value = n;
}

static const size_t nDefaultMaxNumSize = 4;

explicit CScriptNum(const std::vector<unsigned char>& vch, bool fRequireMinimal,
const size_t nMaxNumSize = nDefaultMaxNumSize)
{
if (vch.size() > nMaxNumSize) {
throw scriptnum_error("script number overflow");
}
if (fRequireMinimal && vch.size() > 0) {
// Check that the number is encoded with the minimum possible
// number of bytes.
//
// If the most-significant-byte - excluding the sign bit - is zero
// then we're not minimal. Note how this test also rejects the
// negative-zero encoding, 0x80.
if ((vch.back() & 0x7f) == 0) {
// One exception: if there's more than one byte and the most
// significant bit of the second-most-significant-byte is set
// it would conflict with the sign bit. An example of this case
// is +-255, which encode to 0xff00 and 0xff80 respectively.
// (big-endian).
if (vch.size() <= 1 || (vch[vch.size() - 2] & 0x80) == 0) {
throw scriptnum_error("non-minimally encoded script number");
}
}
}
m_value = set_vch(vch);
}

inline bool operator==(const int64_t& rhs) const { return m_value == rhs; }
inline bool operator!=(const int64_t& rhs) const { return m_value != rhs; }
inline bool operator<=(const int64_t& rhs) const { return m_value <= rhs; }
inline bool operator< (const int64_t& rhs) const { return m_value < rhs; }
inline bool operator>=(const int64_t& rhs) const { return m_value >= rhs; }
inline bool operator> (const int64_t& rhs) const { return m_value > rhs; }

inline bool operator==(const CScriptNum& rhs) const { return operator==(rhs.m_value); }
inline bool operator!=(const CScriptNum& rhs) const { return operator!=(rhs.m_value); }
inline bool operator<=(const CScriptNum& rhs) const { return operator<=(rhs.m_value); }
inline bool operator< (const CScriptNum& rhs) const { return operator< (rhs.m_value); }
inline bool operator>=(const CScriptNum& rhs) const { return operator>=(rhs.m_value); }
inline bool operator> (const CScriptNum& rhs) const { return operator> (rhs.m_value); }

inline CScriptNum operator+( const int64_t& rhs) const { return CScriptNum(m_value + rhs);}
inline CScriptNum operator-( const int64_t& rhs) const { return CScriptNum(m_value - rhs);}
inline CScriptNum operator+( const CScriptNum& rhs) const { return operator+(rhs.m_value); }
inline CScriptNum operator-( const CScriptNum& rhs) const { return operator-(rhs.m_value); }

inline CScriptNum& operator+=( const CScriptNum& rhs) { return operator+=(rhs.m_value); }
inline CScriptNum& operator-=( const CScriptNum& rhs) { return operator-=(rhs.m_value); }

inline CScriptNum operator&( const int64_t& rhs) const { return CScriptNum(m_value & rhs);}
inline CScriptNum operator&( const CScriptNum& rhs) const { return operator&(rhs.m_value); }

inline CScriptNum& operator&=( const CScriptNum& rhs) { return operator&=(rhs.m_value); }

inline CScriptNum operator*(const __int128_t& rhs) const { return CScriptNum(m_value * rhs);}
inline CScriptNum operator*(const CScriptNum& rhs) const { return operator*(rhs.m_value);}

inline CScriptNum operator/(const __int128_t& rhs) const { return CScriptNum(m_value / rhs);}
inline CScriptNum operator/(const CScriptNum& rhs) const { return operator/(rhs.m_value);}

inline CScriptNum operator-() const
{
assert(m_value != std::numeric_limits<__int128_t>::min());
return CScriptNum(-m_value);
}

inline CScriptNum& operator=( const int64_t& rhs)
{
m_value = rhs;
return *this;
}

inline CScriptNum& operator+=( const int64_t& rhs)
{
assert(rhs == 0 || (rhs > 0 && m_value <= std::numeric_limits<__int128_t>::max() - rhs) ||
(rhs < 0 && m_value >= std::numeric_limits<__int128_t>::min() - rhs));
m_value += rhs;
return *this;
}

inline CScriptNum& operator-=( const int64_t& rhs)
{
assert(rhs == 0 || (rhs > 0 && m_value >= std::numeric_limits<__int128_t>::min() + rhs) ||
(rhs < 0 && m_value <= std::numeric_limits<__int128_t>::max() + rhs));
m_value -= rhs;
return *this;
}

inline CScriptNum& operator&=( const int64_t& rhs)
{
m_value &= rhs;
return *this;
}

int getint() const
{
if (m_value > std::numeric_limits<int>::max())
return std::numeric_limits<int>::max();
else if (m_value < std::numeric_limits<int>::min())
return std::numeric_limits<int>::min();
return m_value;
}

int64_t GetInt64() const { return m_value; }
__int128_t GetInt128() const {return m_value; }
std::vector<unsigned char> getvch() const
{
return serialize(m_value);
}

static std::vector<unsigned char> serialize(const __int128_t& value)
{
if(value == 0) {
return std::vector<unsigned char>();
}

std::vector<unsigned char> result;
const bool neg = value < 0;
__uint128_t absvalue = neg ? ~static_cast<__uint128_t>(value) + 1 : static_cast<__uint128_t>(value);

while(absvalue)
{
result.push_back(absvalue & 0xff);
absvalue >>= 8;
}

// - If the most significant byte is >= 0x80 and the value is positive, push a
// new zero-byte to make the significant byte < 0x80 again.

// - If the most significant byte is >= 0x80 and the value is negative, push a
// new 0x80 byte that will be popped off when converting to an integral.

// - If the most significant byte is < 0x80 and the value is negative, add
// 0x80 to it, since it will be subtracted and interpreted as a negative when
// converting to an integral.

if (result.back() & 0x80) {
result.push_back(neg ? 0x80 : 0);
}
else if (neg) {
result.back() |= 0x80;
}

return result;
}

private:
static __int128_t set_vch(const std::vector<unsigned char>& vch)
{
if (vch.empty())
return 0;

__int128_t result = 0;
for (size_t i = 0; i != vch.size(); ++i)
result |= static_cast<__int128_t>(vch[i]) << 8*i;

// If the input vector's most significant byte is 0x80, remove it from
// the result's msb and return a negative.
if (vch.back() & 0x80)
return -((__int128_t)(result & ~(0x80ULL << (8 * (vch.size() - 1)))));

return result;
}

__int128_t m_value;
};
</source>

<source lang="cpp">
case OP_ADD:
case OP_SUB:
case OP_MUL:
case OP_DIV:
case OP_BOOLAND:
case OP_BOOLOR:
case OP_NUMEQUAL:
case OP_NUMEQUALVERIFY:
case OP_NUMNOTEQUAL:
case OP_LESSTHAN:
case OP_GREATERTHAN:
case OP_LESSTHANOREQUAL:
case OP_GREATERTHANOREQUAL:
case OP_MIN:
case OP_MAX:
{
// (x1 x2 -- out)
if (stack.size() < 2)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
CScriptNum bn1 = GetCScriptNum(stacktop(-2), fRequireMinimal, sigversion);
CScriptNum bn2 = GetCScriptNum(stacktop(-1), fRequireMinimal, sigversion);
CScriptNum bn(0);
switch (opcode)
{
case OP_ADD:
bn = bn1 + bn2;
break;

case OP_SUB:
bn = bn1 - bn2;
break;
case OP_MUL:
bn = bn1 * bn2;
break;
case OP_DIV: {
const __int128_t a = bn1.GetInt128();
const __int128_t b = bn2.GetInt128();
if (b == 0) return set_error(serror,SCRIPT_ERR_ARITHMETIC64);
__int128_t r = a % b;
__int128_t q = a / b;

if (r < 0 && b > 0) { r+=b; q-=1;}
else if (r < 0 && b > 0) { r -= b; q+=1; }
//have to pop the stack here for OP_DIV
//as we are pushing two results onto the stack
//quotient and remainder
popstack(stack);
popstack(stack);
bn = CScriptNum(q);
stack.push_back(CScriptNum(r).getvch());
break;
}
case OP_BOOLAND: bn = (bn1 != bnZero && bn2 != bnZero); break;
case OP_BOOLOR: bn = (bn1 != bnZero || bn2 != bnZero); break;
case OP_NUMEQUAL: bn = (bn1 == bn2); break;
case OP_NUMEQUALVERIFY: bn = (bn1 == bn2); break;
case OP_NUMNOTEQUAL: bn = (bn1 != bn2); break;
case OP_LESSTHAN: bn = (bn1 < bn2); break;
case OP_GREATERTHAN: bn = (bn1 > bn2); break;
case OP_LESSTHANOREQUAL: bn = (bn1 <= bn2); break;
case OP_GREATERTHANOREQUAL: bn = (bn1 >= bn2); break;
case OP_MIN: bn = (bn1 < bn2 ? bn1 : bn2); break;
case OP_MAX: bn = (bn1 > bn2 ? bn1 : bn2); break;
default: assert(!"invalid opcode"); break;
}
if (opcode != OP_DIV)
{
popstack(stack);
popstack(stack);
}
stack.push_back(bn.getvch());
</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.

Loading