From 8809f9970fe61d5e3cc8250a8bb7b5e96a28ce23 Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:12:04 -0400 Subject: [PATCH 1/9] Clean up UI shell and move styles out of HTML --- index.html | 349 ++++++++--------------------------------------------- 1 file changed, 52 insertions(+), 297 deletions(-) diff --git a/index.html b/index.html index 938be6b..c407922 100644 --- a/index.html +++ b/index.html @@ -1,310 +1,65 @@ - - - Bitcoin Script Playground - Interactive Learning Platform - - + + + Bitcoin Script Playground + + -
-
-

Bitcoin Script Playground

-

Interactive Learning Platform for Bitcoin Script Development

+
+
+

Bitcoin Script Playground

+

A visual stack-based learning tool for Bitcoin Script concepts.

+
+ +
+ Educational mode: this playground uses a simplified JavaScript interpreter. It is not a Bitcoin Core-compatible consensus engine and must not be used to validate real transactions. +
+ +
+
+

Script Editor

+ + + +
+ + +
-
-
-

Script Editor

- - -
- - - -
+
+

Sample Scripts

+ + + + +
-
-

Sample Scripts:

- - - - -
+
+
-
-
+
+

Stack Visualization

+
+
+ -
-

Stack Visualization

-
-
- -

Stack will appear here when you run a script

-
-
-
-
- -
-

Available Opcodes

-
- -
-
- +
+

Available Opcodes

+
+
+ - + From 057dfe4156da5505b4c4a1803029ae59dc225482 Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:12:17 -0400 Subject: [PATCH 2/9] Add extracted stylesheet --- styles.css | 1 + 1 file changed, 1 insertion(+) create mode 100644 styles.css diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..5b1ea27 --- /dev/null +++ b/styles.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;font-family:Segoe UI,Tahoma,sans-serif;background:linear-gradient(135deg,#667eea,#764ba2);color:#333;min-height:100vh}.container{max-width:1400px;margin:0 auto;padding:20px}.header{text-align:center;color:#fff;margin-bottom:20px}.notice{background:#fff3cd;border:1px solid #ffe69c;padding:12px;border-radius:10px;margin-bottom:20px}.main-content{display:grid;grid-template-columns:1fr 1fr;gap:20px}.panel{background:#fff;border-radius:15px;padding:25px;box-shadow:0 10px 30px rgba(0,0,0,.15)}.script-input{width:100%;min-height:220px;padding:12px;border:2px solid #e2e8f0;border-radius:10px;font-family:monospace}.controls{display:flex;gap:10px;flex-wrap:wrap;margin:15px 0}.btn{border:none;border-radius:8px;padding:12px 18px;cursor:pointer;font-weight:600}.btn-primary{background:#667eea;color:#fff}.btn-secondary{background:#e2e8f0}.btn-success{background:#48bb78;color:#fff}.sample-btn{background:#ed8936;color:#fff;margin:5px 5px 0 0}.stack-container{min-height:320px;border:2px solid #e2e8f0;border-radius:10px;padding:15px;background:#f7fafc}.stack-item{background:#667eea;color:#fff;padding:10px 15px;margin:5px 0;border-radius:8px;display:flex;justify-content:space-between}.opcodes-panel{margin-top:20px}.opcodes-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}.opcode-card{background:#f7fafc;border:2px solid #e2e8f0;border-radius:10px;padding:12px}.status{padding:10px;border-radius:8px;margin-top:10px}.success{background:#c6f6d5}.error{background:#fed7d7}.sr-only{position:absolute;left:-9999px}@media(max-width:768px){.main-content{grid-template-columns:1fr}} \ No newline at end of file From 7577bf5cba22ee44e2fcd35f473b9f1b4931a7c4 Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:12:38 -0400 Subject: [PATCH 3/9] Harden README claims and positioning --- README.md | 196 +++++++----------------------------------------------- 1 file changed, 23 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index 0ccde34..4c1b2fa 100644 --- a/README.md +++ b/README.md @@ -1,192 +1,42 @@ -# 🪙 Bitcoin Script Playground +# Bitcoin Script Playground -> **Interactive Learning Platform for Bitcoin Script Development** +A browser-based educational playground for learning Bitcoin Script fundamentals through opcode execution and stack visualization. -A modern, web-based playground for learning and experimenting with Bitcoin Script. Built with vanilla JavaScript, this tool provides an intuitive interface for understanding Bitcoin's scripting language through hands-on practice. +## Important Note -![Bitcoin Script Playground](https://img.shields.io/badge/Bitcoin-Script%20Playground-orange?style=for-the-badge&logo=bitcoin) -![JavaScript](https://img.shields.io/badge/JavaScript-ES6+-yellow?style=for-the-badge&logo=javascript) -![HTML5](https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=html5&logoColor=white) -![CSS3](https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white) +This project uses a simplified JavaScript interpreter for learning purposes. It is **not** Bitcoin Core-compatible and should not be used to validate real transactions. -## ✨ Features +## Features -### 🎯 **Interactive Script Editor** -- Real-time script execution -- Syntax highlighting and validation -- Error handling with detailed messages -- Keyboard shortcuts (Ctrl+Enter to run) +- Interactive script editor +- Visual stack rendering +- Sample scripts +- Shareable URLs +- Opcode reference cards +- Zero-build static site -### 📚 **Comprehensive Opcode Library** -- **30+ Bitcoin Script opcodes** implemented -- Organized by categories: Stack, Arithmetic, Comparison, Bitwise, Logical, Constants, Verification -- Click-to-insert opcode functionality -- Detailed descriptions for each opcode +## Quick Start -### 🎨 **Visual Stack Management** -- Real-time stack visualization -- Animated stack operations -- Color-coded stack items -- Clear visual feedback - -### 🧩 **Sample Scripts & Puzzles** -- Pre-built examples for different skill levels -- Basic math operations -- Comparison logic -- Multi-signature examples -- Advanced stack manipulation - -### 🔗 **Sharing & Collaboration** -- Generate shareable URLs for scripts -- Copy-to-clipboard functionality -- Easy script distribution - -## 🚀 Quick Start - -### Option 1: Live Demo -Simply open `index.html` in your web browser - no setup required! - -### Option 2: Local Development Server ```bash -# Clone the repository git clone https://github.com/polydeuces32/bitcoin-script-playground.git cd bitcoin-script-playground - -# Start local server python3 -m http.server 8000 -# or -npx serve . - -# Open in browser -open http://localhost:8000 -``` - -## 📖 Usage Examples - -### Basic Arithmetic -```bitcoin-script -OP_1 OP_2 OP_ADD OP_DUP -``` -**Result**: Stack contains `[3, 3]` - -### Comparison Logic -```bitcoin-script -OP_5 OP_3 OP_ADD OP_8 OP_EQUAL -``` -**Result**: Stack contains `[1]` (true) - -### Stack Manipulation -```bitcoin-script -OP_1 OP_2 OP_3 OP_ROT OP_SWAP ``` -**Result**: Stack contains `[2, 1, 3]` - -## 🛠 Supported Opcodes - -### Stack Operations -- `OP_DUP` - Duplicates the top stack item -- `OP_2DUP` - Duplicates the top two stack items -- `OP_DROP` - Removes the top stack item -- `OP_SWAP` - Swaps the top two stack items -- `OP_OVER` - Copies the second-to-top item to the top -- `OP_ROT` - Rotates the top 3 stack items - -### Arithmetic Operations -- `OP_ADD` - Adds the top two stack items -- `OP_SUB` - Subtracts the second item from the first -- `OP_MUL` - Multiplies the top two stack items -- `OP_DIV` - Divides the second item by the first -- `OP_MOD` - Returns the remainder of division - -### Comparison Operations -- `OP_EQUAL` - Returns 1 if top two items are equal -- `OP_EQUALVERIFY` - Same as OP_EQUAL but runs OP_VERIFY afterward -- `OP_1EQUAL` - Returns 1 if input is 1, 0 otherwise -- `OP_0NOTEQUAL` - Returns 1 if input is not 0, 0 otherwise - -### Bitwise Operations -- `OP_AND` - Bitwise AND of the top two items -- `OP_OR` - Bitwise OR of the top two items -- `OP_XOR` - Bitwise XOR of the top two items -- `OP_NOT` - Bitwise NOT of the top item - -### Logical Operations -- `OP_BOOLAND` - Boolean AND of the top two items -- `OP_BOOLOR` - Boolean OR of the top two items - -### Constants -- `OP_0` through `OP_5` - Push numbers 0-5 onto stack - -### Verification -- `OP_VERIFY` - Marks transaction as invalid if top stack value is not true -- `OP_RETURN` - Marks transaction as invalid - -## 🎓 Educational Value - -This playground is perfect for: -- **Bitcoin developers** learning script fundamentals -- **Students** understanding stack-based programming -- **Cryptocurrency enthusiasts** exploring Bitcoin's scripting capabilities -- **Educators** teaching blockchain concepts - -## 🔮 Future Roadmap - -### Phase 1: Enhanced Features -- [ ] More Bitcoin Script opcodes -- [ ] Script validation against Bitcoin rules -- [ ] Transaction simulation -- [ ] Cost estimation - -### Phase 2: Advanced Tools -- [ ] Multi-signature script builder -- [ ] P2SH/P2WSH support -- [ ] Real Bitcoin testnet integration -- [ ] Script optimization suggestions - -### Phase 3: SaaS Platform -- [ ] User accounts and authentication -- [ ] Script library and sharing -- [ ] Collaborative editing -- [ ] API access for developers -- [ ] Premium features and subscriptions - -## 🤝 Contributing - -We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. - -### Development Setup -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/amazing-feature` -3. Make your changes -4. Test thoroughly -5. Commit your changes: `git commit -m 'Add amazing feature'` -6. Push to the branch: `git push origin feature/amazing-feature` -7. Open a Pull Request - -## 📄 License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## 🙏 Acknowledgments - -- Bitcoin Core developers for the original Bitcoin Script specification -- The Bitcoin community for continuous innovation -- Contributors and users who provide feedback and suggestions - -## 📞 Support -- 🐛 **Bug Reports**: [GitHub Issues](https://github.com/polydeuces32/bitcoin-script-playground/issues) -- 💡 **Feature Requests**: [GitHub Discussions](https://github.com/polydeuces32/bitcoin-script-playground/discussions) -- 📧 **Contact**: [polydeuces32@github.com](mailto:polydeuces32@github.com) +Open http://localhost:8000 ---- +## Supported Scope -
+This tool demonstrates common stack, arithmetic, comparison, and verification concepts. Some opcode behavior is intentionally simplified. -**⭐ Star this repository if you find it helpful!** +## Roadmap -Made with ❤️ for the Bitcoin community +- Pure engine/test separation +- More accurate script semantics +- Additional opcode coverage +- Transaction templates +- Testnet teaching mode -[🌐 Live Demo](https://polydeuces32.github.io/bitcoin-script-playground) | [📖 Documentation](https://github.com/polydeuces32/bitcoin-script-playground/wiki) | [🐦 Twitter](https://twitter.com/polydeuces32) +## License -
+MIT From 377d177d0261de60f81b6fd49da34206cca7d3fe Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:13:40 -0400 Subject: [PATCH 4/9] Harden script engine and DOM rendering --- script.js | 910 ++++++++++++++++++++++++------------------------------ 1 file changed, 402 insertions(+), 508 deletions(-) diff --git a/script.js b/script.js index 12da202..685c7e9 100644 --- a/script.js +++ b/script.js @@ -1,535 +1,429 @@ -// Bitcoin Script Playground - Interactive Learning Platform -class BitcoinScriptEngine { - constructor() { - this.stack = []; - this.opcodes = this.initializeOpcodes(); - this.sampleScripts = this.initializeSampleScripts(); - this.initializeUI(); - } - - initializeOpcodes() { - return { - // Stack manipulation - 'OP_DUP': { - name: 'OP_DUP', - description: 'Duplicates the top stack item', - category: 'Stack', - execute: () => this.dup() - }, - 'OP_2DUP': { - name: 'OP_2DUP', - description: 'Duplicates the top two stack items', - category: 'Stack', - execute: () => this.dup2() - }, - 'OP_DROP': { - name: 'OP_DROP', - description: 'Removes the top stack item', - category: 'Stack', - execute: () => this.drop() - }, - 'OP_SWAP': { - name: 'OP_SWAP', - description: 'Swaps the top two stack items', - category: 'Stack', - execute: () => this.swap() - }, - 'OP_OVER': { - name: 'OP_OVER', - description: 'Copies the second-to-top item to the top', - category: 'Stack', - execute: () => this.over() - }, - 'OP_ROT': { - name: 'OP_ROT', - description: 'Rotates the top 3 stack items', - category: 'Stack', - execute: () => this.rot() - }, - - // Arithmetic - 'OP_ADD': { - name: 'OP_ADD', - description: 'Adds the top two stack items', - category: 'Arithmetic', - execute: () => this.add() - }, - 'OP_SUB': { - name: 'OP_SUB', - description: 'Subtracts the second item from the first', - category: 'Arithmetic', - execute: () => this.sub() - }, - 'OP_MUL': { - name: 'OP_MUL', - description: 'Multiplies the top two stack items', - category: 'Arithmetic', - execute: () => this.mul() - }, - 'OP_DIV': { - name: 'OP_DIV', - description: 'Divides the second item by the first', - category: 'Arithmetic', - execute: () => this.div() - }, - 'OP_MOD': { - name: 'OP_MOD', - description: 'Returns the remainder of division', - category: 'Arithmetic', - execute: () => this.mod() - }, - - // Comparison - 'OP_EQUAL': { - name: 'OP_EQUAL', - description: 'Returns 1 if top two items are equal', - category: 'Comparison', - execute: () => this.equal() - }, - 'OP_EQUALVERIFY': { - name: 'OP_EQUALVERIFY', - description: 'Same as OP_EQUAL but runs OP_VERIFY afterward', - category: 'Comparison', - execute: () => this.equalVerify() - }, - 'OP_1EQUAL': { - name: 'OP_1EQUAL', - description: 'Returns 1 if input is 1, 0 otherwise', - category: 'Comparison', - execute: () => this.oneEqual() - }, - 'OP_0NOTEQUAL': { - name: 'OP_0NOTEQUAL', - description: 'Returns 1 if input is not 0, 0 otherwise', - category: 'Comparison', - execute: () => this.zeroNotEqual() - }, - - // Bitwise operations - 'OP_AND': { - name: 'OP_AND', - description: 'Bitwise AND of the top two items', - category: 'Bitwise', - execute: () => this.and() - }, - 'OP_OR': { - name: 'OP_OR', - description: 'Bitwise OR of the top two items', - category: 'Bitwise', - execute: () => this.or() - }, - 'OP_XOR': { - name: 'OP_XOR', - description: 'Bitwise XOR of the top two items', - category: 'Bitwise', - execute: () => this.xor() - }, - 'OP_NOT': { - name: 'OP_NOT', - description: 'Bitwise NOT of the top item', - category: 'Bitwise', - execute: () => this.not() - }, - - // Logical operations - 'OP_BOOLAND': { - name: 'OP_BOOLAND', - description: 'Boolean AND of the top two items', - category: 'Logical', - execute: () => this.boolAnd() - }, - 'OP_BOOLOR': { - name: 'OP_BOOLOR', - description: 'Boolean OR of the top two items', - category: 'Logical', - execute: () => this.boolOr() - }, - - // Constants - 'OP_0': { - name: 'OP_0', - description: 'Pushes empty array onto stack', - category: 'Constants', - execute: () => this.pushZero() - }, - 'OP_1': { - name: 'OP_1', - description: 'Pushes 1 onto stack', - category: 'Constants', - execute: () => this.pushOne() - }, - 'OP_2': { - name: 'OP_2', - description: 'Pushes 2 onto stack', - category: 'Constants', - execute: () => this.pushTwo() - }, - 'OP_3': { - name: 'OP_3', - description: 'Pushes 3 onto stack', - category: 'Constants', - execute: () => this.pushThree() - }, - 'OP_4': { - name: 'OP_4', - description: 'Pushes 4 onto stack', - category: 'Constants', - execute: () => this.pushFour() - }, - 'OP_5': { - name: 'OP_5', - description: 'Pushes 5 onto stack', - category: 'Constants', - execute: () => this.pushFive() - }, - - // Verification - 'OP_VERIFY': { - name: 'OP_VERIFY', - description: 'Marks transaction as invalid if top stack value is not true', - category: 'Verification', - execute: () => this.verify() - }, - 'OP_RETURN': { - name: 'OP_RETURN', - description: 'Marks transaction as invalid', - category: 'Verification', - execute: () => this.returnOp() - } - }; - } - - initializeSampleScripts() { - return { - basic: "OP_1 OP_2 OP_ADD OP_DUP", - comparison: "OP_5 OP_3 OP_ADD OP_8 OP_EQUAL", - multisig: "OP_2 OP_1 OP_1 OP_1 OP_3 OP_CHECKMULTISIG", - advanced: "OP_1 OP_2 OP_3 OP_ROT OP_SWAP OP_ADD OP_DUP OP_EQUAL" - }; - } - - initializeUI() { - this.renderOpcodes(); - } - - renderOpcodes() { - const grid = document.getElementById('opcodesGrid'); - const categories = ['Stack', 'Arithmetic', 'Comparison', 'Bitwise', 'Logical', 'Constants', 'Verification']; - - categories.forEach(category => { - const categoryOpcodes = Object.values(this.opcodes).filter(op => op.category === category); - if (categoryOpcodes.length === 0) return; - - const categoryDiv = document.createElement('div'); - categoryDiv.style.gridColumn = '1 / -1'; - categoryDiv.innerHTML = `

${category}

`; - grid.appendChild(categoryDiv); - - categoryOpcodes.forEach(opcode => { - const card = document.createElement('div'); - card.className = 'opcode-card'; - card.innerHTML = ` -
${opcode.name}
-
${opcode.description}
- `; - card.onclick = () => this.insertOpcode(opcode.name); - grid.appendChild(card); - }); - }); - } - - insertOpcode(opcodeName) { - const textarea = document.getElementById('scriptInput'); - const currentValue = textarea.value; - const newValue = currentValue + (currentValue ? ' ' : '') + opcodeName; - textarea.value = newValue; - textarea.focus(); - } - - // Stack manipulation operations - dup() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - this.stack.push(this.stack[this.stack.length - 1]); - } - - dup2() { - if (this.stack.length < 2) throw new Error('Not enough items on stack'); - const top = this.stack[this.stack.length - 1]; - const second = this.stack[this.stack.length - 2]; - this.stack.push(second, top); - } - - drop() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - this.stack.pop(); - } - - swap() { - if (this.stack.length < 2) throw new Error('Not enough items on stack'); - const top = this.stack.pop(); - const second = this.stack.pop(); - this.stack.push(top, second); - } - - over() { - if (this.stack.length < 2) throw new Error('Not enough items on stack'); - this.stack.push(this.stack[this.stack.length - 2]); - } - - rot() { - if (this.stack.length < 3) throw new Error('Not enough items on stack'); - const top = this.stack.pop(); - const second = this.stack.pop(); - const third = this.stack.pop(); - this.stack.push(second, top, third); - } - - // Arithmetic operations - add() { - if (this.stack.length < 2) throw new Error('Not enough numbers to add'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a + b).toString()); - } - - sub() { - if (this.stack.length < 2) throw new Error('Not enough numbers to subtract'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a - b).toString()); - } - - mul() { - if (this.stack.length < 2) throw new Error('Not enough numbers to multiply'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a * b).toString()); - } - - div() { - if (this.stack.length < 2) throw new Error('Not enough numbers to divide'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - if (b === 0) throw new Error('Division by zero'); - this.stack.push(Math.floor(a / b).toString()); - } - - mod() { - if (this.stack.length < 2) throw new Error('Not enough numbers for modulo'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - if (b === 0) throw new Error('Modulo by zero'); - this.stack.push((a % b).toString()); - } - - // Comparison operations - equal() { - if (this.stack.length < 2) throw new Error('Not enough values to compare'); - const b = this.stack.pop(); - const a = this.stack.pop(); - this.stack.push(a === b ? '1' : '0'); - } - - equalVerify() { - this.equal(); - this.verify(); - } - - oneEqual() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.stack.pop(); - this.stack.push(value === '1' ? '1' : '0'); - } - - zeroNotEqual() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.stack.pop(); - this.stack.push(value !== '0' ? '1' : '0'); - } +'use strict'; - // Bitwise operations - and() { - if (this.stack.length < 2) throw new Error('Not enough values for AND'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a & b).toString()); - } +class BitcoinScriptEngine { + constructor() { + this.stack = []; + this.opcodes = this.initializeOpcodes(); + this.sampleScripts = this.initializeSampleScripts(); + } + + initializeOpcodes() { + return { + OP_DUP: { description: 'Duplicate the top stack item', category: 'Stack', execute: () => this.dup() }, + OP_2DUP: { description: 'Duplicate the top two stack items', category: 'Stack', execute: () => this.dup2() }, + OP_DROP: { description: 'Remove the top stack item', category: 'Stack', execute: () => this.drop() }, + OP_SWAP: { description: 'Swap the top two stack items', category: 'Stack', execute: () => this.swap() }, + OP_OVER: { description: 'Copy the second-to-top item to the top', category: 'Stack', execute: () => this.over() }, + OP_ROT: { description: 'Rotate the top three stack items', category: 'Stack', execute: () => this.rot() }, + + OP_ADD: { description: 'Add the top two numeric values', category: 'Arithmetic', execute: () => this.add() }, + OP_SUB: { description: 'Subtract the top value from the second value', category: 'Arithmetic', execute: () => this.sub() }, + OP_MUL: { description: 'Educational-only multiply operation', category: 'Arithmetic', execute: () => this.mul() }, + OP_DIV: { description: 'Educational-only integer division operation', category: 'Arithmetic', execute: () => this.div() }, + OP_MOD: { description: 'Educational-only modulo operation', category: 'Arithmetic', execute: () => this.mod() }, + + OP_EQUAL: { description: 'Return 1 when the top two items are equal, else 0', category: 'Comparison', execute: () => this.equal() }, + OP_EQUALVERIFY: { description: 'Run OP_EQUAL followed by OP_VERIFY', category: 'Comparison', execute: () => this.equalVerify() }, + OP_1EQUAL: { description: 'Return 1 when the top item equals 1, else 0', category: 'Comparison', execute: () => this.oneEqual() }, + OP_0NOTEQUAL: { description: 'Return 1 when the top item is not 0, else 0', category: 'Comparison', execute: () => this.zeroNotEqual() }, + + OP_AND: { description: 'Educational-only bitwise AND', category: 'Bitwise', execute: () => this.and() }, + OP_OR: { description: 'Educational-only bitwise OR', category: 'Bitwise', execute: () => this.or() }, + OP_XOR: { description: 'Educational-only bitwise XOR', category: 'Bitwise', execute: () => this.xor() }, + OP_NOT: { description: 'Return 1 for 0, otherwise 0', category: 'Logical', execute: () => this.not() }, + OP_BOOLAND: { description: 'Boolean AND of the top two stack items', category: 'Logical', execute: () => this.boolAnd() }, + OP_BOOLOR: { description: 'Boolean OR of the top two stack items', category: 'Logical', execute: () => this.boolOr() }, + + OP_0: { description: 'Push 0 onto the stack', category: 'Constants', execute: () => this.push('0') }, + OP_1: { description: 'Push 1 onto the stack', category: 'Constants', execute: () => this.push('1') }, + OP_2: { description: 'Push 2 onto the stack', category: 'Constants', execute: () => this.push('2') }, + OP_3: { description: 'Push 3 onto the stack', category: 'Constants', execute: () => this.push('3') }, + OP_4: { description: 'Push 4 onto the stack', category: 'Constants', execute: () => this.push('4') }, + OP_5: { description: 'Push 5 onto the stack', category: 'Constants', execute: () => this.push('5') }, + OP_6: { description: 'Push 6 onto the stack', category: 'Constants', execute: () => this.push('6') }, + OP_7: { description: 'Push 7 onto the stack', category: 'Constants', execute: () => this.push('7') }, + OP_8: { description: 'Push 8 onto the stack', category: 'Constants', execute: () => this.push('8') }, + OP_9: { description: 'Push 9 onto the stack', category: 'Constants', execute: () => this.push('9') }, + OP_10: { description: 'Push 10 onto the stack', category: 'Constants', execute: () => this.push('10') }, + + OP_VERIFY: { description: 'Fail unless the top item is true', category: 'Verification', execute: () => this.verify() }, + OP_RETURN: { description: 'Immediately fail execution', category: 'Verification', execute: () => this.returnOp() } + }; + } + + initializeSampleScripts() { + return { + basic: 'OP_1 OP_2 OP_ADD OP_DUP', + comparison: 'OP_5 OP_3 OP_ADD OP_8 OP_EQUAL', + stack: 'OP_1 OP_2 OP_3 OP_ROT OP_SWAP', + verify: 'OP_2 OP_3 OP_ADD OP_5 OP_EQUALVERIFY OP_1' + }; + } + + run(script) { + const tokens = this.tokenize(script); + this.stack = []; + + tokens.forEach((token, index) => { + try { + if (this.opcodes[token]) { + this.opcodes[token].execute(); + return; + } - or() { - if (this.stack.length < 2) throw new Error('Not enough values for OR'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a | b).toString()); - } + if (this.isLiteral(token)) { + this.push(token); + return; + } - xor() { - if (this.stack.length < 2) throw new Error('Not enough values for XOR'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a ^ b).toString()); - } + throw new Error(`Unsupported token: ${token}`); + } catch (error) { + throw new Error(`Token ${index + 1} (${token}): ${error.message}`); + } + }); - not() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.parseNumber(this.stack.pop()); - this.stack.push((~value >>> 0).toString()); - } + return [...this.stack]; + } + + tokenize(script) { + return script.trim().split(/\s+/).filter(Boolean); + } + + isLiteral(token) { + return /^-?\d+$/.test(token); + } + + push(value) { + this.stack.push(String(value)); + } + + requireStackSize(size, message = 'Not enough items on stack') { + if (this.stack.length < size) throw new Error(message); + } + + popNumber() { + this.requireStackSize(1, 'Stack is empty'); + const value = this.stack.pop(); + const number = Number.parseInt(value, 10); + if (!Number.isSafeInteger(number)) throw new Error(`Invalid number: ${value}`); + return number; + } + + isTrue(value) { + return value !== '0' && value !== ''; + } + + dup() { + this.requireStackSize(1, 'Stack is empty'); + this.push(this.stack[this.stack.length - 1]); + } + + dup2() { + this.requireStackSize(2); + const second = this.stack[this.stack.length - 2]; + const top = this.stack[this.stack.length - 1]; + this.push(second); + this.push(top); + } + + drop() { + this.requireStackSize(1, 'Stack is empty'); + this.stack.pop(); + } + + swap() { + this.requireStackSize(2); + const top = this.stack.pop(); + const second = this.stack.pop(); + this.push(top); + this.push(second); + } + + over() { + this.requireStackSize(2); + this.push(this.stack[this.stack.length - 2]); + } + + rot() { + this.requireStackSize(3); + const top = this.stack.pop(); + const second = this.stack.pop(); + const third = this.stack.pop(); + this.push(second); + this.push(top); + this.push(third); + } + + add() { + const b = this.popNumber(); + const a = this.popNumber(); + this.push(a + b); + } + + sub() { + const b = this.popNumber(); + const a = this.popNumber(); + this.push(a - b); + } + + mul() { + const b = this.popNumber(); + const a = this.popNumber(); + this.push(a * b); + } + + div() { + const b = this.popNumber(); + const a = this.popNumber(); + if (b === 0) throw new Error('Division by zero'); + this.push(Math.trunc(a / b)); + } + + mod() { + const b = this.popNumber(); + const a = this.popNumber(); + if (b === 0) throw new Error('Modulo by zero'); + this.push(a % b); + } + + equal() { + this.requireStackSize(2); + const b = this.stack.pop(); + const a = this.stack.pop(); + this.push(a === b ? '1' : '0'); + } + + equalVerify() { + this.equal(); + this.verify(); + } + + oneEqual() { + this.requireStackSize(1, 'Stack is empty'); + this.push(this.stack.pop() === '1' ? '1' : '0'); + } + + zeroNotEqual() { + this.requireStackSize(1, 'Stack is empty'); + this.push(this.stack.pop() !== '0' ? '1' : '0'); + } + + and() { + const b = this.popNumber(); + const a = this.popNumber(); + this.push(a & b); + } + + or() { + const b = this.popNumber(); + const a = this.popNumber(); + this.push(a | b); + } + + xor() { + const b = this.popNumber(); + const a = this.popNumber(); + this.push(a ^ b); + } + + not() { + const value = this.popNumber(); + this.push(value === 0 ? '1' : '0'); + } + + boolAnd() { + const b = this.stack.pop(); + const a = this.stack.pop(); + if (a === undefined || b === undefined) throw new Error('Not enough items on stack'); + this.push(this.isTrue(a) && this.isTrue(b) ? '1' : '0'); + } + + boolOr() { + const b = this.stack.pop(); + const a = this.stack.pop(); + if (a === undefined || b === undefined) throw new Error('Not enough items on stack'); + this.push(this.isTrue(a) || this.isTrue(b) ? '1' : '0'); + } + + verify() { + this.requireStackSize(1, 'Stack is empty'); + const value = this.stack.pop(); + if (!this.isTrue(value)) throw new Error('Verification failed'); + } + + returnOp() { + throw new Error('OP_RETURN failed execution'); + } +} - // Logical operations - boolAnd() { - if (this.stack.length < 2) throw new Error('Not enough values for boolean AND'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a && b) ? '1' : '0'); - } +class PlaygroundUI { + constructor(engine) { + this.engine = engine; + this.scriptInput = document.getElementById('scriptInput'); + this.stackContainer = document.getElementById('stackContainer'); + this.opcodesGrid = document.getElementById('opcodesGrid'); + this.status = document.getElementById('status'); + + this.bindEvents(); + this.renderOpcodes(); + this.renderStack(); + this.loadScriptFromUrl(); + } + + bindEvents() { + document.querySelector('[data-action="run"]').addEventListener('click', () => this.runScript()); + document.querySelector('[data-action="clear"]').addEventListener('click', () => this.clearScript()); + document.querySelector('[data-action="share"]').addEventListener('click', () => this.shareScript()); + + document.querySelectorAll('[data-sample]').forEach((button) => { + button.addEventListener('click', () => this.loadSample(button.dataset.sample)); + }); - boolOr() { - if (this.stack.length < 2) throw new Error('Not enough values for boolean OR'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a || b) ? '1' : '0'); - } + document.addEventListener('keydown', (event) => { + if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { + event.preventDefault(); + this.runScript(); + } + }); + } - // Constants - pushZero() { this.stack.push('0'); } - pushOne() { this.stack.push('1'); } - pushTwo() { this.stack.push('2'); } - pushThree() { this.stack.push('3'); } - pushFour() { this.stack.push('4'); } - pushFive() { this.stack.push('5'); } - - // Verification - verify() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.stack.pop(); - if (value !== '1') throw new Error('Verification failed'); + runScript() { + const script = this.scriptInput.value.trim(); + if (!script) { + this.showStatus('Please enter a script to run.', 'error'); + return; } - returnOp() { - throw new Error('OP_RETURN: Transaction marked as invalid'); + try { + const stack = this.engine.run(script); + this.renderStack(stack); + this.showStatus(`Script executed successfully. Stack items: ${stack.length}.`, 'success'); + } catch (error) { + this.renderStack(this.engine.stack); + this.showStatus(error.message, 'error'); } + } - // Utility functions - parseNumber(value) { - const num = parseInt(value, 10); - if (isNaN(num)) throw new Error(`Invalid number: ${value}`); - return num; - } + clearScript() { + this.scriptInput.value = ''; + this.engine.stack = []; + this.renderStack(); + this.clearStatus(); + } - runScript(script) { - this.stack = []; - const tokens = script.trim().split(/\s+/); - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - - if (this.opcodes[token]) { - try { - this.opcodes[token].execute(); - } catch (error) { - throw new Error(`Error at token ${i + 1} (${token}): ${error.message}`); - } - } else { - // Treat as literal value - this.stack.push(token); - } - } + async shareScript() { + const script = this.scriptInput.value.trim(); + if (!script) { + this.showStatus('No script to share.', 'error'); + return; } - renderStack() { - const container = document.getElementById('stackContainer'); - - if (this.stack.length === 0) { - container.innerHTML = ` -
- -

Stack will appear here when you run a script

-
- `; - return; - } + const url = new URL(window.location.href); + url.searchParams.set('script', script); - container.innerHTML = this.stack.map((item, index) => ` -
- ${item} - #${this.stack.length - index} -
- `).join(''); - } + try { + await navigator.clipboard.writeText(url.toString()); + this.showStatus('Share URL copied to clipboard.', 'success'); + } catch { + this.showStatus('Clipboard unavailable. Copy the URL from the address bar after sharing.', 'error'); + window.history.replaceState(null, '', url.toString()); + } + } + + loadSample(type) { + const script = this.engine.sampleScripts[type]; + if (!script) return; + this.scriptInput.value = script; + this.showStatus(`Loaded sample: ${type}.`, 'success'); + } + + loadScriptFromUrl() { + const params = new URLSearchParams(window.location.search); + const script = params.get('script'); + if (!script) return; + this.scriptInput.value = script; + this.runScript(); + } + + renderOpcodes() { + this.opcodesGrid.textContent = ''; + const categories = [...new Set(Object.values(this.engine.opcodes).map((opcode) => opcode.category))]; + + categories.forEach((category) => { + const title = document.createElement('h3'); + title.textContent = category; + title.style.gridColumn = '1 / -1'; + this.opcodesGrid.appendChild(title); + + Object.entries(this.engine.opcodes) + .filter(([, opcode]) => opcode.category === category) + .forEach(([name, opcode]) => { + const card = document.createElement('button'); + card.type = 'button'; + card.className = 'opcode-card'; + card.addEventListener('click', () => this.insertOpcode(name)); + + const opcodeName = document.createElement('div'); + opcodeName.className = 'opcode-name'; + opcodeName.textContent = name; + + const description = document.createElement('div'); + description.className = 'opcode-desc'; + description.textContent = opcode.description; + + card.appendChild(opcodeName); + card.appendChild(description); + this.opcodesGrid.appendChild(card); + }); + }); + } - showStatus(message, type = 'success') { - const statusDiv = document.getElementById('status'); - statusDiv.innerHTML = `
${message}
`; - setTimeout(() => { - statusDiv.innerHTML = ''; - }, 5000); - } -} + insertOpcode(opcodeName) { + const existing = this.scriptInput.value.trim(); + this.scriptInput.value = existing ? `${existing} ${opcodeName}` : opcodeName; + this.scriptInput.focus(); + } -// Global functions for UI interaction -const engine = new BitcoinScriptEngine(); + renderStack(stack = this.engine.stack) { + this.stackContainer.textContent = ''; -function runScript() { - const script = document.getElementById('scriptInput').value.trim(); - - if (!script) { - engine.showStatus('Please enter a script to run', 'error'); - return; + if (!stack || stack.length === 0) { + const empty = document.createElement('p'); + empty.className = 'empty-stack'; + empty.textContent = 'Stack will appear here when you run a script.'; + this.stackContainer.appendChild(empty); + return; } - try { - engine.runScript(script); - engine.renderStack(); - engine.showStatus(`✅ Script executed successfully! Stack has ${engine.stack.length} items.`); - } catch (error) { - engine.showStatus(`❌ ${error.message}`, 'error'); - engine.renderStack(); - } -} + stack.forEach((item, index) => { + const row = document.createElement('div'); + row.className = 'stack-item'; -function clearScript() { - document.getElementById('scriptInput').value = ''; - engine.stack = []; - engine.renderStack(); - document.getElementById('status').innerHTML = ''; -} + const value = document.createElement('span'); + value.textContent = item; -function shareScript() { - const script = document.getElementById('scriptInput').value; - if (!script) { - engine.showStatus('No script to share', 'error'); - return; - } + const label = document.createElement('span'); + label.textContent = `#${stack.length - index}`; - const url = `${window.location.origin}${window.location.pathname}?script=${encodeURIComponent(script)}`; - navigator.clipboard.writeText(url).then(() => { - engine.showStatus('🔗 Script URL copied to clipboard!'); - }).catch(() => { - engine.showStatus('Failed to copy URL', 'error'); + row.appendChild(value); + row.appendChild(label); + this.stackContainer.appendChild(row); }); + } + + showStatus(message, type) { + this.status.textContent = ''; + const box = document.createElement('div'); + box.className = `status ${type}`; + box.textContent = message; + this.status.appendChild(box); + } + + clearStatus() { + this.status.textContent = ''; + } } -function loadSample(type) { - const script = engine.sampleScripts[type]; - if (script) { - document.getElementById('scriptInput').value = script; - engine.showStatus(`📝 Loaded ${type} sample script`); - } -} - -// Load script from URL parameters -window.addEventListener('load', () => { - const urlParams = new URLSearchParams(window.location.search); - const script = urlParams.get('script'); - if (script) { - document.getElementById('scriptInput').value = decodeURIComponent(script); - runScript(); - } -}); - -// Add keyboard shortcuts -document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'Enter') { - runScript(); - } +window.addEventListener('DOMContentLoaded', () => { + new PlaygroundUI(new BitcoinScriptEngine()); }); From c0392fc8685cefe63e4e04cdc98e2d05c1785de6 Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:15:41 -0400 Subject: [PATCH 5/9] Finalize package metadata and scripts --- package.json | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 8f2548d..8c3f899 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,15 @@ { "name": "bitcoin-script-playground", - "version": "1.0.0", - "description": "Interactive Learning Platform for Bitcoin Script Development", + "version": "1.1.0", + "description": "Educational Bitcoin Script playground with stack visualization", "main": "index.html", "scripts": { "start": "python3 -m http.server 8000", - "serve": "npx serve .", "dev": "npx live-server --port=8000 --open=/index.html", - "build": "echo 'No build process needed for vanilla JS'", - "test": "echo 'No tests specified yet'" + "serve": "npx serve .", + "test": "node -e \"console.log('No automated tests yet')\"" }, - "keywords": [ - "bitcoin", - "script", - "blockchain", - "cryptocurrency", - "education", - "learning", - "playground", - "interactive", - "opcodes", - "stack", - "programming" - ], + "keywords": ["bitcoin","script","education","playground","stack"], "author": "polydeuces32 ", "license": "MIT", "repository": { @@ -38,11 +25,6 @@ "serve": "^14.2.1" }, "engines": { - "node": ">=14.0.0" - }, - "browserslist": [ - "> 1%", - "last 2 versions", - "not dead" - ] + "node": ">=18" + } } From 808b1e47c6de24d5057572fb81a5103b0b500b87 Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:21:30 -0400 Subject: [PATCH 6/9] Add Bitcoin Script Lab terminal layout --- index.html | 69 +++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/index.html b/index.html index c407922..9953546 100644 --- a/index.html +++ b/index.html @@ -3,60 +3,59 @@ - Bitcoin Script Playground - + Bitcoin Script Lab -
-
-

Bitcoin Script Playground

-

A visual stack-based learning tool for Bitcoin Script concepts.

+
+
+
+

POLYDEUCES32 / BTC DEV LAB

+

Bitcoin Script Lab_

+

Trace Bitcoin Script concepts opcode by opcode with live stack state.

+
+
EDUCATIONAL SIMULATOR
-
- Educational mode: this playground uses a simplified JavaScript interpreter. It is not a Bitcoin Core-compatible consensus engine and must not be used to validate real transactions. +
+ This lab uses a simplified JavaScript interpreter. It is not Bitcoin Core-compatible and must not be used to validate real transactions.
-
-
-

Script Editor

+
+
+
01 / SCRIPT INPUT
- - +
- - - + + + + +
-
-

Sample Scripts

- - - - + + + +
+
+
-
+
+
02 / EXECUTION TRACE
+
-
-

Stack Visualization

+
+
03 / STACK STATE
-
-

Available Opcodes

-
+
+
04 / OPCODE REFERENCE
+
From f75c492aa47b499f8ad47b5f1938fcba7dfeec05 Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:21:53 -0400 Subject: [PATCH 7/9] Add terminal UI styles for Script Lab --- styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles.css b/styles.css index 5b1ea27..aa6a5c3 100644 --- a/styles.css +++ b/styles.css @@ -1 +1 @@ -*{box-sizing:border-box}body{margin:0;font-family:Segoe UI,Tahoma,sans-serif;background:linear-gradient(135deg,#667eea,#764ba2);color:#333;min-height:100vh}.container{max-width:1400px;margin:0 auto;padding:20px}.header{text-align:center;color:#fff;margin-bottom:20px}.notice{background:#fff3cd;border:1px solid #ffe69c;padding:12px;border-radius:10px;margin-bottom:20px}.main-content{display:grid;grid-template-columns:1fr 1fr;gap:20px}.panel{background:#fff;border-radius:15px;padding:25px;box-shadow:0 10px 30px rgba(0,0,0,.15)}.script-input{width:100%;min-height:220px;padding:12px;border:2px solid #e2e8f0;border-radius:10px;font-family:monospace}.controls{display:flex;gap:10px;flex-wrap:wrap;margin:15px 0}.btn{border:none;border-radius:8px;padding:12px 18px;cursor:pointer;font-weight:600}.btn-primary{background:#667eea;color:#fff}.btn-secondary{background:#e2e8f0}.btn-success{background:#48bb78;color:#fff}.sample-btn{background:#ed8936;color:#fff;margin:5px 5px 0 0}.stack-container{min-height:320px;border:2px solid #e2e8f0;border-radius:10px;padding:15px;background:#f7fafc}.stack-item{background:#667eea;color:#fff;padding:10px 15px;margin:5px 0;border-radius:8px;display:flex;justify-content:space-between}.opcodes-panel{margin-top:20px}.opcodes-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}.opcode-card{background:#f7fafc;border:2px solid #e2e8f0;border-radius:10px;padding:12px}.status{padding:10px;border-radius:8px;margin-top:10px}.success{background:#c6f6d5}.error{background:#fed7d7}.sr-only{position:absolute;left:-9999px}@media(max-width:768px){.main-content{grid-template-columns:1fr}} \ No newline at end of file +*{box-sizing:border-box}body{margin:0;background:#050505;color:#33ff66;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}button,textarea{font:inherit}.terminal-shell{max-width:1500px;margin:0 auto;padding:24px}.terminal-header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start}.eyebrow{color:#ff9900;font-size:12px;margin:0 0 8px}.subtitle{color:#9ad8a7}.cursor{animation:blink 1s steps(1) infinite}.status-pill{border:1px solid #33ff66;padding:8px 12px}.notice,.panel{border:1px solid #143d1e;background:#081108}.notice{padding:12px;margin:18px 0}.lab-grid{display:grid;grid-template-columns:1.2fr 1fr .8fr;gap:16px}.panel{padding:16px}.panel-title{color:#ff9900;font-size:12px;margin-bottom:10px}.script-input{width:100%;min-height:220px;background:#000;color:#33ff66;border:1px solid #1f5a2d;padding:12px}.controls,.samples{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.btn,.samples button,.opcode-card{background:#0c180c;color:#33ff66;border:1px solid #1f5a2d;padding:10px 12px;cursor:pointer}.btn.primary{border-color:#ff9900;color:#ff9900}.btn.ghost{color:#9ad8a7}.trace-container,.stack-container{min-height:340px;max-height:340px;overflow:auto;border:1px solid #143d1e;padding:10px;background:#000}.trace-row,.stack-item{padding:8px;border-bottom:1px solid #0f2c16}.trace-row.active{background:#102710}.trace-row.error{color:#ff6666}.opcodes-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}.opcode-card{text-align:left}.status-line{margin-top:12px;color:#9ad8a7}.sr-only{position:absolute;left:-9999px}@keyframes blink{50%{opacity:0}}@media(max-width:1100px){.lab-grid{grid-template-columns:1fr}} \ No newline at end of file From c585481b83b4b74ec4844d5de34382327e1da4df Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:23:56 -0400 Subject: [PATCH 8/9] Implement execution trace debugger --- script.js | 443 ++++-------------------------------------------------- 1 file changed, 31 insertions(+), 412 deletions(-) diff --git a/script.js b/script.js index 685c7e9..d246b4a 100644 --- a/script.js +++ b/script.js @@ -9,421 +9,40 @@ class BitcoinScriptEngine { initializeOpcodes() { return { - OP_DUP: { description: 'Duplicate the top stack item', category: 'Stack', execute: () => this.dup() }, - OP_2DUP: { description: 'Duplicate the top two stack items', category: 'Stack', execute: () => this.dup2() }, - OP_DROP: { description: 'Remove the top stack item', category: 'Stack', execute: () => this.drop() }, - OP_SWAP: { description: 'Swap the top two stack items', category: 'Stack', execute: () => this.swap() }, - OP_OVER: { description: 'Copy the second-to-top item to the top', category: 'Stack', execute: () => this.over() }, - OP_ROT: { description: 'Rotate the top three stack items', category: 'Stack', execute: () => this.rot() }, - - OP_ADD: { description: 'Add the top two numeric values', category: 'Arithmetic', execute: () => this.add() }, - OP_SUB: { description: 'Subtract the top value from the second value', category: 'Arithmetic', execute: () => this.sub() }, - OP_MUL: { description: 'Educational-only multiply operation', category: 'Arithmetic', execute: () => this.mul() }, - OP_DIV: { description: 'Educational-only integer division operation', category: 'Arithmetic', execute: () => this.div() }, - OP_MOD: { description: 'Educational-only modulo operation', category: 'Arithmetic', execute: () => this.mod() }, - - OP_EQUAL: { description: 'Return 1 when the top two items are equal, else 0', category: 'Comparison', execute: () => this.equal() }, - OP_EQUALVERIFY: { description: 'Run OP_EQUAL followed by OP_VERIFY', category: 'Comparison', execute: () => this.equalVerify() }, - OP_1EQUAL: { description: 'Return 1 when the top item equals 1, else 0', category: 'Comparison', execute: () => this.oneEqual() }, - OP_0NOTEQUAL: { description: 'Return 1 when the top item is not 0, else 0', category: 'Comparison', execute: () => this.zeroNotEqual() }, - - OP_AND: { description: 'Educational-only bitwise AND', category: 'Bitwise', execute: () => this.and() }, - OP_OR: { description: 'Educational-only bitwise OR', category: 'Bitwise', execute: () => this.or() }, - OP_XOR: { description: 'Educational-only bitwise XOR', category: 'Bitwise', execute: () => this.xor() }, - OP_NOT: { description: 'Return 1 for 0, otherwise 0', category: 'Logical', execute: () => this.not() }, - OP_BOOLAND: { description: 'Boolean AND of the top two stack items', category: 'Logical', execute: () => this.boolAnd() }, - OP_BOOLOR: { description: 'Boolean OR of the top two stack items', category: 'Logical', execute: () => this.boolOr() }, - - OP_0: { description: 'Push 0 onto the stack', category: 'Constants', execute: () => this.push('0') }, - OP_1: { description: 'Push 1 onto the stack', category: 'Constants', execute: () => this.push('1') }, - OP_2: { description: 'Push 2 onto the stack', category: 'Constants', execute: () => this.push('2') }, - OP_3: { description: 'Push 3 onto the stack', category: 'Constants', execute: () => this.push('3') }, - OP_4: { description: 'Push 4 onto the stack', category: 'Constants', execute: () => this.push('4') }, - OP_5: { description: 'Push 5 onto the stack', category: 'Constants', execute: () => this.push('5') }, - OP_6: { description: 'Push 6 onto the stack', category: 'Constants', execute: () => this.push('6') }, - OP_7: { description: 'Push 7 onto the stack', category: 'Constants', execute: () => this.push('7') }, - OP_8: { description: 'Push 8 onto the stack', category: 'Constants', execute: () => this.push('8') }, - OP_9: { description: 'Push 9 onto the stack', category: 'Constants', execute: () => this.push('9') }, - OP_10: { description: 'Push 10 onto the stack', category: 'Constants', execute: () => this.push('10') }, - - OP_VERIFY: { description: 'Fail unless the top item is true', category: 'Verification', execute: () => this.verify() }, - OP_RETURN: { description: 'Immediately fail execution', category: 'Verification', execute: () => this.returnOp() } + OP_DUP:{description:'Duplicate the top stack item',category:'Stack',execute:()=>this.dup()},OP_2DUP:{description:'Duplicate the top two stack items',category:'Stack',execute:()=>this.dup2()},OP_DROP:{description:'Remove the top stack item',category:'Stack',execute:()=>this.drop()},OP_SWAP:{description:'Swap the top two stack items',category:'Stack',execute:()=>this.swap()},OP_OVER:{description:'Copy the second-to-top item to the top',category:'Stack',execute:()=>this.over()},OP_ROT:{description:'Rotate the top three stack items',category:'Stack',execute:()=>this.rot()}, + OP_ADD:{description:'Add the top two numeric values',category:'Arithmetic',execute:()=>this.add()},OP_SUB:{description:'Subtract top value from second value',category:'Arithmetic',execute:()=>this.sub()},OP_MUL:{description:'Educational-only multiply operation',category:'Arithmetic',execute:()=>this.mul()},OP_DIV:{description:'Educational-only integer division',category:'Arithmetic',execute:()=>this.div()},OP_MOD:{description:'Educational-only modulo',category:'Arithmetic',execute:()=>this.mod()}, + OP_EQUAL:{description:'Return 1 when top two items match',category:'Comparison',execute:()=>this.equal()},OP_EQUALVERIFY:{description:'Run OP_EQUAL then OP_VERIFY',category:'Comparison',execute:()=>this.equalVerify()},OP_1EQUAL:{description:'Return 1 when top item equals 1',category:'Comparison',execute:()=>this.oneEqual()},OP_0NOTEQUAL:{description:'Return 1 when top item is not 0',category:'Comparison',execute:()=>this.zeroNotEqual()}, + OP_AND:{description:'Educational-only bitwise AND',category:'Bitwise',execute:()=>this.and()},OP_OR:{description:'Educational-only bitwise OR',category:'Bitwise',execute:()=>this.or()},OP_XOR:{description:'Educational-only bitwise XOR',category:'Bitwise',execute:()=>this.xor()},OP_NOT:{description:'Return 1 for 0, otherwise 0',category:'Logical',execute:()=>this.not()},OP_BOOLAND:{description:'Boolean AND of top two items',category:'Logical',execute:()=>this.boolAnd()},OP_BOOLOR:{description:'Boolean OR of top two items',category:'Logical',execute:()=>this.boolOr()}, + OP_0:{description:'Push 0',category:'Constants',execute:()=>this.push('0')},OP_1:{description:'Push 1',category:'Constants',execute:()=>this.push('1')},OP_2:{description:'Push 2',category:'Constants',execute:()=>this.push('2')},OP_3:{description:'Push 3',category:'Constants',execute:()=>this.push('3')},OP_4:{description:'Push 4',category:'Constants',execute:()=>this.push('4')},OP_5:{description:'Push 5',category:'Constants',execute:()=>this.push('5')},OP_6:{description:'Push 6',category:'Constants',execute:()=>this.push('6')},OP_7:{description:'Push 7',category:'Constants',execute:()=>this.push('7')},OP_8:{description:'Push 8',category:'Constants',execute:()=>this.push('8')},OP_9:{description:'Push 9',category:'Constants',execute:()=>this.push('9')},OP_10:{description:'Push 10',category:'Constants',execute:()=>this.push('10')}, + OP_VERIFY:{description:'Fail unless top item is true',category:'Verification',execute:()=>this.verify()},OP_RETURN:{description:'Immediately fail execution',category:'Verification',execute:()=>this.returnOp()} }; } - initializeSampleScripts() { - return { - basic: 'OP_1 OP_2 OP_ADD OP_DUP', - comparison: 'OP_5 OP_3 OP_ADD OP_8 OP_EQUAL', - stack: 'OP_1 OP_2 OP_3 OP_ROT OP_SWAP', - verify: 'OP_2 OP_3 OP_ADD OP_5 OP_EQUALVERIFY OP_1' - }; - } - - run(script) { - const tokens = this.tokenize(script); - this.stack = []; - - tokens.forEach((token, index) => { - try { - if (this.opcodes[token]) { - this.opcodes[token].execute(); - return; - } - - if (this.isLiteral(token)) { - this.push(token); - return; - } - - throw new Error(`Unsupported token: ${token}`); - } catch (error) { - throw new Error(`Token ${index + 1} (${token}): ${error.message}`); - } - }); - - return [...this.stack]; - } - - tokenize(script) { - return script.trim().split(/\s+/).filter(Boolean); - } - - isLiteral(token) { - return /^-?\d+$/.test(token); - } - - push(value) { - this.stack.push(String(value)); - } - - requireStackSize(size, message = 'Not enough items on stack') { - if (this.stack.length < size) throw new Error(message); - } - - popNumber() { - this.requireStackSize(1, 'Stack is empty'); - const value = this.stack.pop(); - const number = Number.parseInt(value, 10); - if (!Number.isSafeInteger(number)) throw new Error(`Invalid number: ${value}`); - return number; - } - - isTrue(value) { - return value !== '0' && value !== ''; - } - - dup() { - this.requireStackSize(1, 'Stack is empty'); - this.push(this.stack[this.stack.length - 1]); - } - - dup2() { - this.requireStackSize(2); - const second = this.stack[this.stack.length - 2]; - const top = this.stack[this.stack.length - 1]; - this.push(second); - this.push(top); - } - - drop() { - this.requireStackSize(1, 'Stack is empty'); - this.stack.pop(); - } - - swap() { - this.requireStackSize(2); - const top = this.stack.pop(); - const second = this.stack.pop(); - this.push(top); - this.push(second); - } - - over() { - this.requireStackSize(2); - this.push(this.stack[this.stack.length - 2]); - } - - rot() { - this.requireStackSize(3); - const top = this.stack.pop(); - const second = this.stack.pop(); - const third = this.stack.pop(); - this.push(second); - this.push(top); - this.push(third); - } - - add() { - const b = this.popNumber(); - const a = this.popNumber(); - this.push(a + b); - } - - sub() { - const b = this.popNumber(); - const a = this.popNumber(); - this.push(a - b); - } - - mul() { - const b = this.popNumber(); - const a = this.popNumber(); - this.push(a * b); - } - - div() { - const b = this.popNumber(); - const a = this.popNumber(); - if (b === 0) throw new Error('Division by zero'); - this.push(Math.trunc(a / b)); - } - - mod() { - const b = this.popNumber(); - const a = this.popNumber(); - if (b === 0) throw new Error('Modulo by zero'); - this.push(a % b); - } - - equal() { - this.requireStackSize(2); - const b = this.stack.pop(); - const a = this.stack.pop(); - this.push(a === b ? '1' : '0'); - } - - equalVerify() { - this.equal(); - this.verify(); - } - - oneEqual() { - this.requireStackSize(1, 'Stack is empty'); - this.push(this.stack.pop() === '1' ? '1' : '0'); - } - - zeroNotEqual() { - this.requireStackSize(1, 'Stack is empty'); - this.push(this.stack.pop() !== '0' ? '1' : '0'); - } - - and() { - const b = this.popNumber(); - const a = this.popNumber(); - this.push(a & b); - } - - or() { - const b = this.popNumber(); - const a = this.popNumber(); - this.push(a | b); - } - - xor() { - const b = this.popNumber(); - const a = this.popNumber(); - this.push(a ^ b); - } - - not() { - const value = this.popNumber(); - this.push(value === 0 ? '1' : '0'); - } - - boolAnd() { - const b = this.stack.pop(); - const a = this.stack.pop(); - if (a === undefined || b === undefined) throw new Error('Not enough items on stack'); - this.push(this.isTrue(a) && this.isTrue(b) ? '1' : '0'); - } - - boolOr() { - const b = this.stack.pop(); - const a = this.stack.pop(); - if (a === undefined || b === undefined) throw new Error('Not enough items on stack'); - this.push(this.isTrue(a) || this.isTrue(b) ? '1' : '0'); - } - - verify() { - this.requireStackSize(1, 'Stack is empty'); - const value = this.stack.pop(); - if (!this.isTrue(value)) throw new Error('Verification failed'); - } - - returnOp() { - throw new Error('OP_RETURN failed execution'); - } + initializeSampleScripts(){return{basic:'OP_1 OP_2 OP_ADD OP_DUP',comparison:'OP_5 OP_3 OP_ADD OP_8 OP_EQUAL',stack:'OP_1 OP_2 OP_3 OP_ROT OP_SWAP',verify:'OP_2 OP_3 OP_ADD OP_5 OP_EQUALVERIFY OP_1'};} + tokenize(script){return script.trim().split(/\s+/).filter(Boolean);} isLiteral(t){return/^-?\d+$/.test(t);} push(v){this.stack.push(String(v));} + requireStackSize(n,m='Not enough items on stack'){if(this.stack.length this.runScript()); - document.querySelector('[data-action="clear"]').addEventListener('click', () => this.clearScript()); - document.querySelector('[data-action="share"]').addEventListener('click', () => this.shareScript()); - - document.querySelectorAll('[data-sample]').forEach((button) => { - button.addEventListener('click', () => this.loadSample(button.dataset.sample)); - }); - - document.addEventListener('keydown', (event) => { - if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { - event.preventDefault(); - this.runScript(); - } - }); - } - - runScript() { - const script = this.scriptInput.value.trim(); - if (!script) { - this.showStatus('Please enter a script to run.', 'error'); - return; - } - - try { - const stack = this.engine.run(script); - this.renderStack(stack); - this.showStatus(`Script executed successfully. Stack items: ${stack.length}.`, 'success'); - } catch (error) { - this.renderStack(this.engine.stack); - this.showStatus(error.message, 'error'); - } - } - - clearScript() { - this.scriptInput.value = ''; - this.engine.stack = []; - this.renderStack(); - this.clearStatus(); - } - - async shareScript() { - const script = this.scriptInput.value.trim(); - if (!script) { - this.showStatus('No script to share.', 'error'); - return; - } - - const url = new URL(window.location.href); - url.searchParams.set('script', script); - - try { - await navigator.clipboard.writeText(url.toString()); - this.showStatus('Share URL copied to clipboard.', 'success'); - } catch { - this.showStatus('Clipboard unavailable. Copy the URL from the address bar after sharing.', 'error'); - window.history.replaceState(null, '', url.toString()); - } - } - - loadSample(type) { - const script = this.engine.sampleScripts[type]; - if (!script) return; - this.scriptInput.value = script; - this.showStatus(`Loaded sample: ${type}.`, 'success'); - } - - loadScriptFromUrl() { - const params = new URLSearchParams(window.location.search); - const script = params.get('script'); - if (!script) return; - this.scriptInput.value = script; - this.runScript(); - } - - renderOpcodes() { - this.opcodesGrid.textContent = ''; - const categories = [...new Set(Object.values(this.engine.opcodes).map((opcode) => opcode.category))]; - - categories.forEach((category) => { - const title = document.createElement('h3'); - title.textContent = category; - title.style.gridColumn = '1 / -1'; - this.opcodesGrid.appendChild(title); - - Object.entries(this.engine.opcodes) - .filter(([, opcode]) => opcode.category === category) - .forEach(([name, opcode]) => { - const card = document.createElement('button'); - card.type = 'button'; - card.className = 'opcode-card'; - card.addEventListener('click', () => this.insertOpcode(name)); - - const opcodeName = document.createElement('div'); - opcodeName.className = 'opcode-name'; - opcodeName.textContent = name; - - const description = document.createElement('div'); - description.className = 'opcode-desc'; - description.textContent = opcode.description; - - card.appendChild(opcodeName); - card.appendChild(description); - this.opcodesGrid.appendChild(card); - }); - }); - } - - insertOpcode(opcodeName) { - const existing = this.scriptInput.value.trim(); - this.scriptInput.value = existing ? `${existing} ${opcodeName}` : opcodeName; - this.scriptInput.focus(); - } - - renderStack(stack = this.engine.stack) { - this.stackContainer.textContent = ''; - - if (!stack || stack.length === 0) { - const empty = document.createElement('p'); - empty.className = 'empty-stack'; - empty.textContent = 'Stack will appear here when you run a script.'; - this.stackContainer.appendChild(empty); - return; - } - - stack.forEach((item, index) => { - const row = document.createElement('div'); - row.className = 'stack-item'; - - const value = document.createElement('span'); - value.textContent = item; - - const label = document.createElement('span'); - label.textContent = `#${stack.length - index}`; - - row.appendChild(value); - row.appendChild(label); - this.stackContainer.appendChild(row); - }); - } - - showStatus(message, type) { - this.status.textContent = ''; - const box = document.createElement('div'); - box.className = `status ${type}`; - box.textContent = message; - this.status.appendChild(box); - } - - clearStatus() { - this.status.textContent = ''; - } +class PlaygroundUI{ + constructor(engine){this.engine=engine;this.trace=[];this.activeStep=-1;this.scriptInput=document.getElementById('scriptInput');this.stackContainer=document.getElementById('stackContainer');this.traceContainer=document.getElementById('traceContainer');this.opcodesGrid=document.getElementById('opcodesGrid');this.status=document.getElementById('status');this.bindEvents();this.renderOpcodes();this.resetTrace();this.loadScriptFromUrl();} + bindEvents(){document.querySelector('[data-action="run"]').addEventListener('click',()=>this.runAll());document.querySelector('[data-action="step"]').addEventListener('click',()=>this.step());document.querySelector('[data-action="reset"]').addEventListener('click',()=>this.resetTrace());document.querySelector('[data-action="clear"]').addEventListener('click',()=>this.clearScript());document.querySelector('[data-action="share"]').addEventListener('click',()=>this.shareScript());document.querySelectorAll('[data-sample]').forEach(b=>b.addEventListener('click',()=>this.loadSample(b.dataset.sample)));document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();this.runAll();}});} + compileTrace(){const script=this.scriptInput.value.trim();if(!script){this.showStatus('Enter a script first.','error');return false;}this.trace=this.engine.buildTrace(script);this.activeStep=-1;return true;} + runAll(){if(!this.compileTrace())return;this.activeStep=this.trace.length-1;this.renderTrace();this.renderStack(this.trace.at(-1)?.after||[]);const failed=this.trace.find(t=>!t.ok);this.showStatus(failed?`Execution stopped: ${failed.error}`:`Execution complete. Steps: ${this.trace.length}.`,failed?'error':'success');} + step(){if(this.trace.length===0&&!this.compileTrace())return;if(this.activeStep{const row=document.createElement('div');row.className=`trace-row${i===this.activeStep?' active':''}${!t.ok?' error':''}`;const main=document.createElement('div');main.textContent=`${String(t.index+1).padStart(2,'0')} > ${t.token}`;const before=document.createElement('small');before.textContent=`before [${t.before.join(', ')}] -> after [${t.after.join(', ')}]`;row.append(main,before);if(t.error){const err=document.createElement('small');err.textContent=`error: ${t.error}`;row.appendChild(err);}this.traceContainer.appendChild(row);});} + renderStack(stack=[]){this.stackContainer.textContent='';if(stack.length===0){const p=document.createElement('p');p.textContent='[] empty stack';this.stackContainer.appendChild(p);return;}stack.forEach((item,i)=>{const row=document.createElement('div');row.className='stack-item';const v=document.createElement('span');v.textContent=item;const l=document.createElement('span');l.textContent=`#${stack.length-i}`;row.append(v,l);this.stackContainer.appendChild(row);});} + renderOpcodes(){this.opcodesGrid.textContent='';const cats=[...new Set(Object.values(this.engine.opcodes).map(o=>o.category))];cats.forEach(c=>{const h=document.createElement('h3');h.textContent=c;h.style.gridColumn='1/-1';this.opcodesGrid.appendChild(h);Object.entries(this.engine.opcodes).filter(([,o])=>o.category===c).forEach(([name,o])=>{const card=document.createElement('button');card.type='button';card.className='opcode-card';card.addEventListener('click',()=>this.insertOpcode(name));const n=document.createElement('div');n.textContent=name;const d=document.createElement('small');d.textContent=o.description;card.append(n,d);this.opcodesGrid.appendChild(card);});});} + insertOpcode(name){const current=this.scriptInput.value.trim();this.scriptInput.value=current?`${current} ${name}`:name;this.resetTrace();this.scriptInput.focus();} + showStatus(msg,type){this.status.textContent=`${type.toUpperCase()}: ${msg}`;} clearStatus(){this.status.textContent='';} } - -window.addEventListener('DOMContentLoaded', () => { - new PlaygroundUI(new BitcoinScriptEngine()); -}); +window.addEventListener('DOMContentLoaded',()=>new PlaygroundUI(new BitcoinScriptEngine())); From cb4a1f732cdd77937a6a38bdf96344c9e2561324 Mon Sep 17 00:00:00 2001 From: Giancarlo Vizhnay <75271806+polydeuces32@users.noreply.github.com> Date: Fri, 1 May 2026 11:25:30 -0400 Subject: [PATCH 9/9] Prepare Bitcoin Script Lab v2 launch README --- README.md | 58 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4c1b2fa..87511b4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ -# Bitcoin Script Playground +# Bitcoin Script Lab -A browser-based educational playground for learning Bitcoin Script fundamentals through opcode execution and stack visualization. +A terminal-style educational lab for learning Bitcoin Script concepts through opcode-by-opcode execution tracing and live stack visualization. -## Important Note +## v2 Highlights -This project uses a simplified JavaScript interpreter for learning purposes. It is **not** Bitcoin Core-compatible and should not be used to validate real transactions. +- Terminal-inspired Bitcoin developer UI +- Step-by-step execution trace +- Run All / Step / Reset Trace controls +- Live stack state panel +- Opcode reference panel +- Shareable scripts through URL parameters +- Zero-build static web app -## Features +## Important Scope Note -- Interactive script editor -- Visual stack rendering -- Sample scripts -- Shareable URLs -- Opcode reference cards -- Zero-build static site +Bitcoin Script Lab uses a simplified JavaScript interpreter for learning purposes. It is **not** Bitcoin Core-compatible and must not be used to validate real transactions. ## Quick Start @@ -23,19 +24,38 @@ cd bitcoin-script-playground python3 -m http.server 8000 ``` -Open http://localhost:8000 +Open: -## Supported Scope +```text +http://localhost:8000 +``` + +## Example Script + +```text +OP_1 OP_2 OP_ADD OP_DUP +``` + +Trace result: + +```text +01 > OP_1 [] -> [1] +02 > OP_2 [1] -> [1, 2] +03 > OP_ADD [1, 2] -> [3] +04 > OP_DUP [3] -> [3, 3] +``` + +## Why This Exists -This tool demonstrates common stack, arithmetic, comparison, and verification concepts. Some opcode behavior is intentionally simplified. +Bitcoin Script is stack-based and difficult to understand from static documentation alone. This project makes the stack transitions visible so beginners can learn by running and stepping through scripts. ## Roadmap -- Pure engine/test separation -- More accurate script semantics -- Additional opcode coverage -- Transaction templates -- Testnet teaching mode +- Animated stack transitions +- Real transaction template examples +- Hashlock and timelock teaching modes +- Miniscript learning mode +- Better automated tests ## License