Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.d/tuck-stack-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed

- **OP_TUCK obeys the shared 1,000-item stack limit again, and RIPEMD160 /
HASH256 reject an over-limit witness v0 stack instead of panicking.**
Both regressed on master after v0.8.0, when the per-opcode stack check
was folded into pushes.
18 changes: 9 additions & 9 deletions crates/rbitcoin-consensus/src/script/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,10 @@ pub(crate) fn eval_script(
}

let rm = ctx.minimal_data;
// `push` enforces the shared stack limit. A later check would
// not see a larger stack: every growth goes through `push`,
// and OP_TOALTSTACK only moves an element.
// `push` enforces the shared stack limit. Every growth goes
// through `push`, and OP_TOALTSTACK only moves an element. A
// witness v0 stack can start above the limit, so a pop and a
// push can still fail.
match code {
// OP_0 is PushBytes, not an opcode. OP_RESERVED, OP_VER,
// OP_VERIF, and OP_RETURN fail from the unknown-opcode arm
Expand Down Expand Up @@ -720,7 +721,9 @@ pub(crate) fn eval_script(
0x7d => {
require_n(stack, 2)?;
let v = stack[stack.len() - 1].clone();
stack.insert(stack.len() - 2, v);
push(stack, altstack.len(), v)?;
let n = stack.len();
stack.swap(n - 3, n - 2);
}
0x82 => {
require_n(stack, 1)?;
Expand Down Expand Up @@ -808,9 +811,7 @@ pub(crate) fn eval_script(
let v = pop(stack)?;
use bitcoin::hashes::ripemd160;
let digest = ripemd160::Hash::hash(&v).to_byte_array();
// Pop-then-one-push cannot exceed the limit `push` already
// enforced on the way to a full stack. The digest is 20 bytes.
push(stack, altstack.len(), digest.to_vec()).expect("ripemd160 fits");
push(stack, altstack.len(), digest.to_vec())?;
}
0xa7 => {
// OP_SHA1 is consensus-enabled (was stubbed → would fail post-milestone).
Expand All @@ -829,8 +830,7 @@ pub(crate) fn eval_script(
let v = pop(stack)?;
use bitcoin::hashes::sha256d;
let digest = sha256d::Hash::hash(&v).to_byte_array();
// Same stack shape as RIPEMD160: one pop, one 32-byte push.
push(stack, altstack.len(), digest.to_vec()).expect("hash256 fits");
push(stack, altstack.len(), digest.to_vec())?;
}
0xab => {
// Tapscript: instruction index. Base/v0: scriptCode after this opcode.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ through `VerifyScript`. Legacy rows are bare scripts with no flags.
| `stack-1001-v0` | Witness v0 rejects a 1,001st stack push. A successful 1,000-deep witness script cannot also return to one element within the 201 opcode budget, so the accept side is the legacy and tapscript rows. |
| `stack-exec-1000-tapscript` | Tapscript enforces the shared 1,000 cap after each opcode. 1,000 pushes and 999 drops leave one true item. |
| `stack-exec-1001-tapscript` | Tapscript rejects the 1,001st push during execution. |
| `tuck-1000-legacy` | OP_TUCK counts toward the shared 1,000 cap. 999 pushes and TUCK make 1,000 and accept. |
| `tuck-1001-legacy` | OP_TUCK on a stack of 1,000 makes 1,001 and rejects. |
| `tuck-1000-tapscript` | Tapscript OP_TUCK to 1,000, then 999 drops leave one true item. |
| `tuck-1001-tapscript` | Tapscript OP_TUCK to 1,001 rejects, even though later drops would leave one true item. |
| `initial-1001-ripemd160-v0` | Witness v0 has no initial stack check. RIPEMD160 on 1,001 items leaves 1,001 and rejects. |
| `initial-1001-hash256-v0` | Witness v0 has no initial stack check. HASH256 on 1,001 items leaves 1,001 and rejects. |
| `disabled-cat-unexec-legacy` | OP_CAT (0x7e) rejects in legacy and witness v0 even inside an unexecuted IF. In tapscript that byte is OP_SUCCESS and accepts. |
| `disabled-cat-unexec-v0` | OP_CAT (0x7e) rejects in legacy and witness v0 even inside an unexecuted IF. In tapscript that byte is OP_SUCCESS and accepts. |
| `disabled-cat-unexec-tapscript` | OP_CAT (0x7e) rejects in legacy and witness v0 even inside an unexecuted IF. In tapscript that byte is OP_SUCCESS and accepts. |
Expand Down
Loading
Loading