-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
base: master
Are you sure you want to change the base?
Changes from all commits
d0aafd9
54350b5
b11e06e
8bbf627
5d52ff3
464ce8e
9da332c
b5d0fe5
5713905
1c8a5af
243db3f
d1c90b0
5bebdd1
bc417cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
Author: Chris Stewart <[email protected]> | ||
Comments-Summary: No comments yet. | ||
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0364 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi Ethan thanks for your continued interest in this topic.
👍
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
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. | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe: