Compare commits
17
Commits
4cc20edf96
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb829d6441 | ||
|
|
cd253ad6bb | ||
|
|
7ac0ae3188 | ||
|
|
1eda777047 | ||
|
|
e21ddf45a4 | ||
|
|
2a4f01481b | ||
|
|
1079129e48 | ||
|
|
e668fcf9a0 | ||
|
|
a1890e650b | ||
|
|
50288eb30c | ||
|
|
818da97ea5 | ||
|
|
9f3636d232 | ||
|
|
dadfb7983a | ||
|
|
6b874bf8ba | ||
|
|
fce9ae4d6f | ||
|
|
40172b8eaf | ||
|
|
92e353dcda |
+4
-1
@@ -1,2 +1,5 @@
|
||||
cache/
|
||||
onchain/cache/
|
||||
onchain/out/
|
||||
out/
|
||||
cache/
|
||||
lib/forge-std
|
||||
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
[submodule "lib/forge-std"]
|
||||
path = lib/forge-std
|
||||
|
||||
[submodule "onchain/lib/forge-std"]
|
||||
path = onchain/lib/forge-std
|
||||
url = https://github.com/foundry-rs/forge-std
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
me: Bounty
|
||||
about: Bounty
|
||||
title: ''
|
||||
labels: bounty
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Bounty
|
||||
|
||||
## Scope
|
||||
<!-- A list of specific things which should be done to deliver the bounty. These could be seen as requirements to verify/review bounty against -->
|
||||
|
||||
-
|
||||
-
|
||||
-
|
||||
|
||||
## Deliverables
|
||||
<!-- Artifacts produced as the result of this bounty. Something that could be verified/reviewed. Some examples: updated code, deployment made, blog post published, public event conducted etc -->
|
||||
|
||||
-
|
||||
-
|
||||
|
||||
## Bounty Owner/Gardener
|
||||
<!-- The Role who is responsible for the bounty. -->
|
||||
<@your github username> as **<Role>**
|
||||
|
||||
## Gain for the Role
|
||||
<!-- How the completion of this bounty helps to pursue the Role's purpose -->
|
||||
|
||||
## Roles
|
||||
bounty gardener: <@your git username> / X DAI <!-- likely shouldn't be a % but rather flat -->
|
||||
bounty worker: name / share
|
||||
bounty reviewer: name / share
|
||||
|
||||
## Gardener checklist
|
||||
|
||||
- [ ] I have attached one of the `size-` labels to this bounty
|
||||
|
||||
<!--
|
||||
# Bounty sizes
|
||||
XS / 200 DAI / effort ~ 3h
|
||||
S / 350 DAI / effort ~5h
|
||||
M / 550 DAI / effort ~8h
|
||||
L / 900 DAI / effort ~13h
|
||||
XL / 1400 DAI / effort ~21h
|
||||
-->
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
# Interpreter.md
|
||||
|
||||
## Overview
|
||||
|
||||
This repository provides a **Push3-like interpreter** implemented in Solidity. It uses a _token-based bytecode_ approach to parse programs from a `bytes` array (the `code`) into typed **descriptors**, which are then executed by the interpreter.
|
||||
|
||||
**Push3** is a stack-based language originally designed for genetic programming and autoconstructive evolution. Each data type (e.g. `INT_LITERAL`, `FLOAT`, `BOOLEAN`, etc.) has its own stack. The **`EXEC`** stack manages the flow of execution.
|
||||
|
||||
**In this implementation**:
|
||||
- We store code in a single `bytes` array (`code`).
|
||||
- We parse sublists and integer literals from that array.
|
||||
- We push instructions and literals onto an execution stack.
|
||||
- We pop them in LIFO order and execute them.
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Token-Based Parsing**: We define small tokens (`0x00` => NOOP, `0x01` => INTEGER_PLUS, `0x02` => INT_LITERAL, `0x03` => SUBLIST). If we see `INT_LITERAL`, we read 4 bytes for a 32-bit integer; if we see `SUBLIST`, we read 2 bytes for the sublist length, then parse it recursively.
|
||||
2. **Descriptor System**: We represent instructions, literals, and sublists using 256-bit “descriptors.” Each descriptor encodes:
|
||||
- A `tag` (top 8 bits): e.g. `INT_LITERAL`, `INSTRUCTION`, `SUBLIST`.
|
||||
- `offset` and `length` for sublists.
|
||||
- A leftover field (e.g. for storing integer data).
|
||||
3. **Reversing Items**: When we pop a sublist descriptor, we parse it into descriptors and then push them onto the `EXEC` stack in reverse order. This ensures the list’s leftmost token executes first, consistent with the Push3 spec.
|
||||
4. **Separation of Parsing and Execution**:
|
||||
- **Parsing**: Transforms raw byte tokens into typed descriptors (instructions, literals, or nested sublists).
|
||||
- **Execution**: The `runInterpreter` function repeatedly pops from `EXEC`, decides if it’s an instruction or literal or sublist, and acts accordingly.
|
||||
|
||||
## Code Structure
|
||||
|
||||
### 1. **Enums**
|
||||
|
||||
- **`CodeTag`**:
|
||||
- `NO_TAG` (unused),
|
||||
- `INSTRUCTION`,
|
||||
- `INT_LITERAL`,
|
||||
- `SUBLIST`.
|
||||
|
||||
- **`OpCode`**:
|
||||
- `NOOP`,
|
||||
- `INTEGER_PLUS`.
|
||||
|
||||
### 2. **Descriptor Layout**
|
||||
|
||||
A **256-bit** word has:
|
||||
```
|
||||
[ top 8 bits | offset(32 bits) | length(32 bits) | leftover(184 bits) ]
|
||||
```
|
||||
- `tag`: The top 8 bits (e.g. `INT_LITERAL`).
|
||||
- `offset, length`: If `tag = SUBLIST`, these define which bytes in `code` represent the sub-sublists.
|
||||
- `leftover`: If `tag = INT_LITERAL`, we store the 32-bit integer in the bottom 32 bits of leftover. If `tag = INSTRUCTION`, we store the opcode (`NOOP`, `INTEGER_PLUS`) in the lowest 8 bits of leftover.
|
||||
|
||||
### 3. **Helper Reads**
|
||||
|
||||
- **`readUint32(code, start)`**: Copies 4 bytes from calldata into a small buffer, returns the final `uint32`. By shifting bits if needed, we ensure we get the integer from the correct part of the loaded word.
|
||||
- **`readUint16(code, start)`**: Similar logic for 2 bytes.
|
||||
|
||||
### 4. **`parseSublist` Function**
|
||||
|
||||
When we see a sublist descriptor (`tag=SUBLIST`), we parse the slice `[off..off+len]` in the `bytes code`. We iterate token by token:
|
||||
- `0x00` => `NOOP`
|
||||
- `0x01` => `INTEGER_PLUS`
|
||||
- `0x02` => read 4 bytes => `INT_LITERAL`
|
||||
- `0x03` => read 2 bytes => sub-sublist length => build a sub-descriptor
|
||||
|
||||
We accumulate these descriptors in an array. Then, later, we push them _in reverse_ onto the `EXEC` stack.
|
||||
|
||||
### 5. **`runInterpreter` Function**
|
||||
|
||||
1. Loads initial CODE, EXEC, and INT stacks from the provided arrays.
|
||||
2. While `EXEC` is non-empty:
|
||||
- Pop the top descriptor.
|
||||
- If `INSTRUCTION`, decode the opcode (e.g. `INTEGER_PLUS`) and do it (pop top 2 ints, push their sum).
|
||||
- If `INT_LITERAL`, push that integer onto the int stack.
|
||||
- If `SUBLIST`, parse that sublist into descriptors, then push those in reverse.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Add New Instructions**
|
||||
- Expand `OpCode` enum (e.g. `INTEGER_MINUS`, `INTEGER_MUL`, etc.).
|
||||
- Write a `makeInstruction` or `makeIntLiteral` function to build the descriptor for it.
|
||||
- In the main loop, implement the logic (pop the correct number of integers, do the operation, push the result).
|
||||
|
||||
2. **Support More Data Types**
|
||||
- For instance, a `FLOAT` or `BOOLEAN` stack.
|
||||
- Add a new tag (`FLOAT_LITERAL`?), new instructions (`FLOAT.PLUS`?), etc.
|
||||
|
||||
3. **Improve Parsing**
|
||||
- If you want 256-bit literals, define a new token that reads 32 bytes, or do other expansions (like `CODE` type).
|
||||
- Or define advanced sublist structures (like nested `( ( ) )` expansions).
|
||||
|
||||
4. **Gas Optimization**
|
||||
- Check how large arrays are allocated (like in `parseSublist`).
|
||||
- Possibly implement step-by-step execution if large code exceeds block gas limits.
|
||||
|
||||
5. **Add Logging & Debug Tools**
|
||||
- Use `console.log(...)` to trace tokens, track final states, or measure performance.
|
||||
|
||||
6. **Test Coverage**
|
||||
- Provide more unit tests in Foundry for advanced instructions, nested sublists, edge cases with zero-length sublists, etc.
|
||||
|
||||
## Next Steps
|
||||
|
||||
### A. Extending the VM
|
||||
|
||||
A “full” Push3 VM might include:
|
||||
- **Multiple Data Stacks**: `BOOLEAN`, `FLOAT`, `NAME`, etc.
|
||||
- **NAME Binding**: Let `NAME`s reference values or code.
|
||||
- **Control Structures**: `EXEC.IF`, `EXEC.DO*COUNT`, or combinators like `EXEC.Y`.
|
||||
|
||||
### B. Example Project
|
||||
|
||||
You could use this interpreter in:
|
||||
- A genetic programming environment, where random code is generated and mutated.
|
||||
- A puzzle solver that runs tiny “scripts” in EVM-like constraints.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This `Push3Interpreter` is a **minimal** scaffold for a token-based, list-expanding interpreter with typed descriptors. By following the patterns for instructions, sublists, and typed stacks, you can easily expand it into a more complete VM. We welcome contributions in the form of new instructions, data types, and performance optimizations.
|
||||
|
||||
1. **Clone** this repo.
|
||||
2. **Check** the `test/` folder for Foundry-based tests.
|
||||
3. **Add** or modify instructions & sublists in `parseSublist` and `runInterpreter`.
|
||||
4. **Run** `forge test --via-ir -vv` to see logs and confirm your changes.
|
||||
|
||||
Enjoy exploring the **Push3** style VM in Solidity!
|
||||
@@ -1,6 +1,14 @@
|
||||
# **SoLUSH**: A Push 3 Virtual Machine in Solidity
|
||||
|
||||
Welcome to **SoLUSH 3** (pronounced like **"slush"** without the "o", composed from **S**o**L**idity and p**USH** 3), a Solidity implementation of the [Push 3 stack machine](http://faculty.hampshire.edu/lspector/push3-description.html), designed to bring evolved genetic algorithms on-chain. By enabling the execution of Push 3 programs directly within Ethereum smart contracts, SoLush 3 paves the way for decentralized, adaptive systems to address complex optimization problems.
|
||||
Welcome to **SoLUSH 3** (pronounced like **"slush"** without the "o", composed from **S**o**L**idity and p**USH** 3), a **Push3-like interpreter** implemented in Solidity. It uses a _token-based bytecode_ approach to parse programs from a `bytes` array (the `code`) into typed **descriptors**, which are then executed by the interpreter.
|
||||
|
||||
**Push3** is a stack-based language originally designed for genetic programming and autoconstructive evolution. Each data type (e.g. `INT_LITERAL`, `FLOAT`, `BOOLEAN`, etc.) has its own stack. The **`EXEC`** stack manages the flow of execution.
|
||||
|
||||
**In this implementation**:
|
||||
- We store code in a single `bytes` array (`code`).
|
||||
- We parse sublists and integer literals from that array.
|
||||
- We push instructions and literals onto an execution stack.
|
||||
- We pop them in LIFO order and execute them.
|
||||
|
||||
## 🌟 Vision
|
||||
|
||||
@@ -12,19 +20,97 @@ SoLUSH is a key step toward realizing the vision of running decentralized evolut
|
||||
|
||||
To understand the broader vision, check out the [Vision Document](vision.md).
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Token-Based Parsing**: We define small tokens (`0x00` => NOOP, `0x01` => INTEGER_PLUS, `0x02` => INT_LITERAL, `0x03` => SUBLIST). If we see `INT_LITERAL`, we read 4 bytes for a 32-bit integer; if we see `SUBLIST`, we read 2 bytes for the sublist length, then parse it recursively.
|
||||
2. **Descriptor System**: We represent instructions, literals, and sublists using 256-bit “descriptors.” Each descriptor encodes:
|
||||
- A `tag` (top 8 bits): e.g. `INT_LITERAL`, `INSTRUCTION`, `SUBLIST`.
|
||||
- `offset` and `length` for sublists.
|
||||
- A leftover field (e.g. for storing integer data).
|
||||
3. **Reversing Items**: When we pop a sublist descriptor, we parse it into descriptors and then push them onto the `EXEC` stack in reverse order. This ensures the list’s leftmost token executes first, consistent with the Push3 spec.
|
||||
4. **Separation of Parsing and Execution**:
|
||||
- **Parsing**: Transforms raw byte tokens into typed descriptors (instructions, literals, or nested sublists).
|
||||
- **Execution**: The `runInterpreter` function repeatedly pops from `EXEC`, decides if it’s an instruction or literal or sublist, and acts accordingly.
|
||||
|
||||
## Code Structure
|
||||
|
||||
### 1. **Enums**
|
||||
|
||||
- **`CodeTag`**:
|
||||
- `NO_TAG` (unused),
|
||||
- `INSTRUCTION`,
|
||||
- `INT_LITERAL`,
|
||||
- `SUBLIST`.
|
||||
|
||||
- **`OpCode`**:
|
||||
- `NOOP`,
|
||||
- `INTEGER_PLUS`.
|
||||
|
||||
### 2. **Descriptor Layout**
|
||||
|
||||
A **256-bit** word has:
|
||||
```
|
||||
[ top 8 bits | offset(32 bits) | length(32 bits) | leftover(184 bits) ]
|
||||
```
|
||||
- `tag`: The top 8 bits (e.g. `INT_LITERAL`).
|
||||
- `offset, length`: If `tag = SUBLIST`, these define which bytes in `code` represent the sub-sublists.
|
||||
- `leftover`: If `tag = INT_LITERAL`, we store the 32-bit integer in the bottom 32 bits of leftover. If `tag = INSTRUCTION`, we store the opcode (`NOOP`, `INTEGER_PLUS`) in the lowest 8 bits of leftover.
|
||||
|
||||
### 3. **Helper Reads**
|
||||
|
||||
- **`readUint32(code, start)`**: Copies 4 bytes from calldata into a small buffer, returns the final `uint32`. By shifting bits if needed, we ensure we get the integer from the correct part of the loaded word.
|
||||
- **`readUint16(code, start)`**: Similar logic for 2 bytes.
|
||||
|
||||
### 4. **`parseSublist` Function**
|
||||
|
||||
When we see a sublist descriptor (`tag=SUBLIST`), we parse the slice `[off..off+len]` in the `bytes code`. We iterate token by token:
|
||||
- `0x00` => `NOOP`
|
||||
- `0x01` => `INTEGER_PLUS`
|
||||
- `0x02` => read 4 bytes => `INT_LITERAL`
|
||||
- `0x03` => read 2 bytes => sub-sublist length => build a sub-descriptor
|
||||
|
||||
We accumulate these descriptors in an array. Then, later, we push them _in reverse_ onto the `EXEC` stack.
|
||||
|
||||
### 5. **`runInterpreter` Function**
|
||||
|
||||
1. Loads initial CODE, EXEC, and INT stacks from the provided arrays.
|
||||
2. While `EXEC` is non-empty:
|
||||
- Pop the top descriptor.
|
||||
- If `INSTRUCTION`, decode the opcode (e.g. `INTEGER_PLUS`) and do it (pop top 2 ints, push their sum).
|
||||
- If `INT_LITERAL`, push that integer onto the int stack.
|
||||
- If `SUBLIST`, parse that sublist into descriptors, then push those in reverse.
|
||||
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
### A. Extending the VM
|
||||
|
||||
A “full” Push3 VM might include:
|
||||
- **Multiple Data Stacks**: `BOOLEAN`, `FLOAT`, `NAME`, etc.
|
||||
- **NAME Binding**: Let `NAME`s reference values or code.
|
||||
- **Control Structures**: `EXEC.IF`, `EXEC.DO*COUNT`, or combinators like `EXEC.Y`.
|
||||
|
||||
### B. Example Project
|
||||
|
||||
You could use this interpreter in:
|
||||
- A genetic programming environment, where random code is generated and mutated.
|
||||
- A puzzle solver that runs tiny “scripts” in EVM-like constraints.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
SoLUSH 3 is in active development. To contribute or test the functionality, follow these steps:
|
||||
This `Push3Interpreter` is a **minimal** scaffold for a token-based, list-expanding interpreter with typed descriptors. By following the patterns for instructions, sublists, and typed stacks, you can easily expand it into a more complete VM. We welcome contributions in the form of new instructions, data types, and performance optimizations.
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/your-username/SoLUSH.git
|
||||
cd SoLUSH
|
||||
```
|
||||
2. Install dependencies (if applicable).
|
||||
3. Review the examples and test cases to understand how Push 3 programs are executed on the VM.
|
||||
1. **Clone** this repo.
|
||||
2. **Check** the `test/` folder for Foundry-based tests.
|
||||
3. **Add** or modify instructions & sublists in `parseSublist` and `runInterpreter`.
|
||||
4. **Run** `forge test --via-ir -vv` to see logs and confirm your changes.
|
||||
|
||||
|
||||
### How to contribute
|
||||
|
||||
Check the [Issues](../../../issues) section to find tasks to tackle.
|
||||
|
||||
## 🛠️ Roadmap
|
||||
|
||||
- **Core Features:**
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 113 KiB |
@@ -0,0 +1,152 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "forge-std/console.sol";
|
||||
import {Push3Interpreter} from "../src/Push3Interpreter.sol";
|
||||
|
||||
/**
|
||||
* @title Push3IntAdapter
|
||||
* @dev An adapter contract for our Push3Interpreter that provides
|
||||
* various integer-only I/O function overloads. We now create
|
||||
* a SUBLIST descriptor for the entire code array, placing it
|
||||
* on the EXEC stack. We ignore finalCodeStack/finalExecStack
|
||||
* to avoid compiler warnings.
|
||||
*/
|
||||
contract Push3IntAdapter {
|
||||
Push3Interpreter public interpreter;
|
||||
|
||||
constructor(address _interpreter) {
|
||||
interpreter = Push3Interpreter(_interpreter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Utility function to build a single sublist descriptor
|
||||
* covering the entire `code` array, offset=0, length=code.length.
|
||||
*/
|
||||
function _makeFullSublistDescriptor(bytes calldata code)
|
||||
internal
|
||||
view
|
||||
returns (uint256)
|
||||
{
|
||||
// CodeTag.SUBLIST is typically enum = 3, but
|
||||
// let's fetch it via
|
||||
// (uint8)Push3Interpreter.CodeTag.SUBLIST
|
||||
// or we can directly pass 3 if we know the numeric value.
|
||||
|
||||
// We'll do:
|
||||
return interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST, // the tag
|
||||
0, // offset
|
||||
uint32(code.length), // length of entire code array
|
||||
0 // leftover bits=0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Single int input, single int output.
|
||||
* 1) Build a sublist descriptor for the entire code,
|
||||
* 2) Place that descriptor in the EXEC stack,
|
||||
* 3) Preload `in1` on the integer stack,
|
||||
* 4) Run the interpreter,
|
||||
* 5) Return top of final int stack.
|
||||
*/
|
||||
function run1In1Out(bytes calldata code, int256 in1)
|
||||
external
|
||||
view
|
||||
returns (int256 out1)
|
||||
{
|
||||
// 1) Build the sublist descriptor
|
||||
uint256 fullDesc = _makeFullSublistDescriptor(code);
|
||||
|
||||
// 2) EXEC stack of size 1
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = fullDesc;
|
||||
|
||||
// No code stack
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
|
||||
// 3) Preload int stack
|
||||
int256[] memory initIntStack = new int256[](1);
|
||||
initIntStack[0] = in1;
|
||||
|
||||
// 4) Run
|
||||
(, , int256[] memory finalIntStack) =
|
||||
interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// 5) Return top of final int stack
|
||||
require(finalIntStack.length > 0, "No integer output produced");
|
||||
out1 = finalIntStack[finalIntStack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Two ints input, single int output
|
||||
*/
|
||||
function run2In1Out(bytes calldata code, int256 in1, int256 in2)
|
||||
external
|
||||
view
|
||||
returns (int256 out1)
|
||||
{
|
||||
uint256 fullDesc = _makeFullSublistDescriptor(code);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = fullDesc;
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
|
||||
int256[] memory initIntStack = new int256[](2);
|
||||
initIntStack[0] = in1;
|
||||
initIntStack[1] = in2;
|
||||
|
||||
(, , int256[] memory finalIntStack) =
|
||||
interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
require(finalIntStack.length > 0, "No integer output");
|
||||
|
||||
out1 = finalIntStack[finalIntStack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Single int input, two int outputs
|
||||
*/
|
||||
function run1In2Out(bytes calldata code, int256 in1)
|
||||
external
|
||||
view
|
||||
returns (int256 out1, int256 out2)
|
||||
{
|
||||
uint256 fullDesc = _makeFullSublistDescriptor(code);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = fullDesc;
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
|
||||
int256[] memory initIntStack = new int256[](1);
|
||||
initIntStack[0] = in1;
|
||||
|
||||
(, , int256[] memory finalIntStack) =
|
||||
interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
require(finalIntStack.length >= 2, "Not enough outputs");
|
||||
out1 = finalIntStack[finalIntStack.length - 1];
|
||||
out2 = finalIntStack[finalIntStack.length - 2];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Zero int input, returns entire final int stack
|
||||
*/
|
||||
function run0InNOut(bytes calldata code)
|
||||
external
|
||||
view
|
||||
returns (int256[] memory)
|
||||
{
|
||||
uint256 fullDesc = _makeFullSublistDescriptor(code);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = fullDesc;
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
(, , int256[] memory finalIntStack) =
|
||||
interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
return finalIntStack;
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import "forge-std/console.sol";
|
||||
/**
|
||||
* @title Push3Interpreter
|
||||
* @dev Demonstrates a minimal Push3-like interpreter that uses
|
||||
* a token-based bytecode approach. We fix the assembly
|
||||
* endianness issues by copying exact bytes for 2- and 4-byte reads.
|
||||
* a token-based bytecode approach.
|
||||
*/
|
||||
contract Push3Interpreter {
|
||||
// -----------------------------------------------------
|
||||
@@ -22,7 +21,11 @@ contract Push3Interpreter {
|
||||
|
||||
enum OpCode {
|
||||
NOOP, // 0
|
||||
INTEGER_PLUS // 1
|
||||
INTEGER_PLUS, // 1
|
||||
INTEGER_MINUS, // 2
|
||||
INTEGER_MULT, // 3
|
||||
INTEGER_DUP, // 4
|
||||
INTEGER_POP // 5
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
@@ -111,12 +114,15 @@ contract Push3Interpreter {
|
||||
*/
|
||||
function readUint16(bytes calldata code, uint32 start) internal pure returns (uint16 val) {
|
||||
require(start + 2 <= code.length, "readUint16 out of range");
|
||||
uint256 word;
|
||||
assembly {
|
||||
let buf := mload(0x40)
|
||||
calldatacopy(buf, add(code.offset, start), 2)
|
||||
let word := mload(buf)
|
||||
word := mload(buf)
|
||||
val := and(word, 0xffff)
|
||||
}
|
||||
// Not sure this is correct
|
||||
val = uint16(word >> 240);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
@@ -128,6 +134,7 @@ contract Push3Interpreter {
|
||||
* 0x01 => INTEGER_PLUS
|
||||
* 0x02 => INT_LITERAL => read next 4 bytes => int32
|
||||
* 0x03 => SUBLIST => read next 2 bytes => subLen => parse that
|
||||
* 0x04 => INTEGER_MINUS
|
||||
*/
|
||||
function parseSublist(bytes calldata code, uint32 off, uint32 len)
|
||||
internal
|
||||
@@ -187,6 +194,24 @@ contract Push3Interpreter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (tokenType == 0x04) {
|
||||
// INTEGER_PLUS
|
||||
temp[count] = makeInstruction(OpCode.INTEGER_MINUS);
|
||||
count++;
|
||||
}
|
||||
else if (tokenType == 0x05) {
|
||||
// INTEGER_PLUS
|
||||
temp[count] = makeInstruction(OpCode.INTEGER_MULT);
|
||||
count++;
|
||||
}
|
||||
else if (tokenType == 0x06) {
|
||||
temp[count] = makeInstruction(OpCode.INTEGER_DUP);
|
||||
count++;
|
||||
}
|
||||
else if (tokenType == 0x07) {
|
||||
temp[count] = makeInstruction(OpCode.INTEGER_POP);
|
||||
count++;
|
||||
}
|
||||
else {
|
||||
// unknown => NOOP
|
||||
temp[count] = makeInstruction(OpCode.NOOP);
|
||||
@@ -260,6 +285,38 @@ contract Push3Interpreter {
|
||||
intTop++;
|
||||
}
|
||||
}
|
||||
else if (op == OpCode.INTEGER_MINUS) {
|
||||
// pop top 2 => minus
|
||||
if (intTop >= 2) {
|
||||
int256 a = intStack[intTop - 1];
|
||||
int256 b = intStack[intTop - 2];
|
||||
intTop -= 2;
|
||||
intStack[intTop] = b - a;
|
||||
intTop++;
|
||||
}
|
||||
}
|
||||
else if (op == OpCode.INTEGER_MULT) {
|
||||
// pop top 2 => minus
|
||||
if (intTop >= 2) {
|
||||
int256 a = intStack[intTop - 1];
|
||||
int256 b = intStack[intTop - 2];
|
||||
intTop -= 2;
|
||||
intStack[intTop] = b * a;
|
||||
intTop++;
|
||||
}
|
||||
}
|
||||
else if (op == OpCode.INTEGER_DUP) {
|
||||
if (intTop >= 1) {
|
||||
int256 a = intStack[intTop - 1];
|
||||
intStack[intTop] = a;
|
||||
intTop++;
|
||||
}
|
||||
}
|
||||
else if (op == OpCode.INTEGER_POP) {
|
||||
if (intTop >= 1) {
|
||||
intTop -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (tag == CodeTag.INT_LITERAL) {
|
||||
int32 val = extractInt32(topDesc);
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {Push3Interpreter} from "../src/Push3Interpreter.sol";
|
||||
import {Push3IntAdapter} from "../src/Push3IntAdapter.sol";
|
||||
|
||||
/**
|
||||
* @title Push3IntAdapterTest
|
||||
* @dev Foundry test for the Push3IntAdapter contract,
|
||||
* which composes with the Push3Interpreter.
|
||||
*/
|
||||
contract Push3IntAdapterTest is Test {
|
||||
Push3Interpreter public interpreter;
|
||||
Push3IntAdapter public adapter;
|
||||
|
||||
function setUp() public {
|
||||
// 1) Deploy the interpreter
|
||||
interpreter = new Push3Interpreter();
|
||||
|
||||
// 2) Deploy the adapter, referencing the interpreter address
|
||||
adapter = new Push3IntAdapter(address(interpreter));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test run1In1Out with a minimal code array.
|
||||
* For example, we might do a program that just leaves its input unchanged
|
||||
* (like a NOOP).
|
||||
*/
|
||||
function test_run1In1Out_noop() public view {
|
||||
bytes memory code = hex"03000100";
|
||||
|
||||
int256 inputVal = 123;
|
||||
int256 result = adapter.run1In1Out(code, inputVal);
|
||||
|
||||
// We expect the code does nothing => output == input
|
||||
assertEq(result, inputVal, "Expected same value out");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test run2In1Out. We'll push 5, 7, do an op that leaves 12.
|
||||
*/
|
||||
function test_run2In1Out_plus() public view {
|
||||
bytes memory code = hex"03000101";
|
||||
|
||||
int256 in1 = 5;
|
||||
int256 in2 = 7;
|
||||
int256 result = adapter.run2In1Out(code, in1, in2);
|
||||
|
||||
assertEq(result, 12, "Expected 5 + 7 = 12");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test run1In2Out. Push a constant to a stack with 1.
|
||||
* For now, we do a placeholder code.
|
||||
*/
|
||||
function test_run1In2Out() public view {
|
||||
bytes memory code = hex"0300050200000003";
|
||||
|
||||
int256 inVal = 42;
|
||||
(int256 top, int256 secondTop) = adapter.run1In2Out(code, inVal);
|
||||
|
||||
assertEq(top, 3, "top stack val mismatch");
|
||||
assertEq(secondTop, 42, "second top stack val mismatch");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test run0InNOut.
|
||||
* We'll feed a code that pushes some constants, and see the final stack.
|
||||
*/
|
||||
function test_run0InNOut() public view {
|
||||
|
||||
bytes memory code = hex"03000A02000000030200000005";
|
||||
|
||||
int256[] memory stackResult = adapter.run0InNOut(code);
|
||||
|
||||
assertEq(stackResult.length, 2, "Expected 2 final stack items");
|
||||
assertEq(stackResult[0], 3, "Item 0 mismatch");
|
||||
assertEq(stackResult[1], 5, "Item 1 mismatch");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import "forge-std/console.sol";
|
||||
import {Push3Interpreter} from "../src/Push3Interpreter.sol";
|
||||
|
||||
/**
|
||||
* @title Push3InterpreterTest
|
||||
* @dev Foundry test contract for the updated Push3Interpreter
|
||||
*/
|
||||
contract Push3InterpreterTest is Test {
|
||||
Push3Interpreter public interpreter;
|
||||
|
||||
function setUp() public {
|
||||
interpreter = new Push3Interpreter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test a small program that:
|
||||
* 1) Pushes literal 10 (int32)
|
||||
* 2) Pushes literal 32 (int32)
|
||||
* 3) Calls INTEGER_PLUS
|
||||
*
|
||||
* We'll store everything in a single SUBLIST:
|
||||
* 0x03 => SUBLIST
|
||||
* 0x00 0x0B => subLen=11
|
||||
* Then the next 11 bytes are:
|
||||
* 0x02 + (00 00 00 0A) => INT_LITERAL(10)
|
||||
* 0x02 + (00 00 00 20) => INT_LITERAL(32)
|
||||
* 0x01 => INTEGER_PLUS
|
||||
*
|
||||
* So total = 1 + 2 + (1+4 + 1+4 + 1) = 14 bytes
|
||||
* => 0x03 00 0B 02 00 00 00 0A 02 00 00 00 20 01
|
||||
*/
|
||||
function test_LiteralsAndPlus() public view {
|
||||
// The final code array (14 bytes):
|
||||
// [0] = 0x03
|
||||
// [1..2] = 0x000B
|
||||
// [3] = 0x02
|
||||
// [4..7] = 0x0000000A => 10
|
||||
// [8] = 0x02
|
||||
// [9..12] = 0x00000020 => 32
|
||||
// [13] = 0x01 => plus
|
||||
|
||||
bytes memory code = hex"03000B020000000A020000002001";
|
||||
|
||||
// Or fully concatenated:
|
||||
// 0x03 00 0B 02 00 00 00 0A 02 00 00 00 20 01
|
||||
|
||||
// We'll parse offset=0, length=14
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0, // offset
|
||||
14, // length
|
||||
0
|
||||
);
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
// Run it
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// Should produce finalIntStack = [42]
|
||||
assertEq(finalIntStack.length, 1, "finalIntStack should have length 1");
|
||||
assertEq(finalIntStack[0], 42, "Should be 10+32=42");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test pushing literal 10, literal 3, then INTEGER_MINUS => (10 - 3) = 7
|
||||
*
|
||||
* The plan:
|
||||
* 0x03 => SUBLIST token
|
||||
* 0x00 0x0B => subLen=11
|
||||
*
|
||||
* Then 11 bytes:
|
||||
* 0x02 + (00 00 00 0A) => INT_LITERAL(10)
|
||||
* 0x02 + (00 00 00 03) => INT_LITERAL(3)
|
||||
* 0x04 => INTEGER_MINUS (assuming 0x04 is assigned to minus)
|
||||
*/
|
||||
function test_Minus() public view {
|
||||
// We'll produce 14 total bytes, same structure as the plus example
|
||||
// Format: [0x03, 0x000B, 0x02(10), 0x03(3), 0x04 => minus]
|
||||
|
||||
bytes memory code = hex"03000B020000000A020000000304";
|
||||
// Breaking it down:
|
||||
// 0x03 => SUBLIST
|
||||
// 0x00 0x0B => length=11
|
||||
// 0x02 0000000A => INT_LITERAL(10)
|
||||
// 0x02 00000003 => INT_LITERAL(3)
|
||||
// 0x04 => INTEGER_MINUS
|
||||
|
||||
// We'll parse offset=0, length=14
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0, // offset
|
||||
14, // length
|
||||
0
|
||||
);
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
// Run
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// Check final result => should be [7]
|
||||
assertEq(finalIntStack.length, 1, "finalIntStack should have length 1");
|
||||
assertEq(finalIntStack[0], 7, "Should be 10 - 3 = 7");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test pushing literal 10, literal 3, then INTEGER_MULT => (10 * 3) = 30
|
||||
*
|
||||
* The plan:
|
||||
* 0x03 => SUBLIST token
|
||||
* 0x00 0x0B => subLen=11
|
||||
*
|
||||
* Then 11 bytes:
|
||||
* 0x02 + (00 00 00 0A) => INT_LITERAL(10)
|
||||
* 0x02 + (00 00 00 03) => INT_LITERAL(3)
|
||||
* 0x05 => INTEGER_MULT
|
||||
*/
|
||||
function test_Mult() public view {
|
||||
// We'll produce 14 total bytes, same structure as the plus example
|
||||
// Format: [0x03, 0x000B, 0x02(10), 0x03(3), 0x05 => mul]
|
||||
|
||||
bytes memory code = hex"03000B020000000A020000000305";
|
||||
// Breaking it down:
|
||||
// 0x03 => SUBLIST
|
||||
// 0x00 0x0B => length=11
|
||||
// 0x02 0000000A => INT_LITERAL(10)
|
||||
// 0x02 00000003 => INT_LITERAL(3)
|
||||
// 0x05 => INTEGER_MUL
|
||||
|
||||
// We'll parse offset=0, length=14
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0, // offset
|
||||
14, // length
|
||||
0
|
||||
);
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
// Run
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// Check final result => should be [30]
|
||||
assertEq(finalIntStack.length, 1, "finalIntStack should have length 1");
|
||||
assertEq(finalIntStack[0], 30, "Should be 10 * 3 = 30");
|
||||
}
|
||||
|
||||
function test_Dup() public view {
|
||||
|
||||
bytes memory code = hex"030006020000000A06";
|
||||
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0,
|
||||
9,
|
||||
0
|
||||
);
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
// Run
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
assertEq(finalIntStack.length, 2, "finalIntStack should have length 1");
|
||||
assertEq(finalIntStack[0], finalIntStack[1], "The top 2 values on the stack should be equal.");
|
||||
}
|
||||
|
||||
function test_Pop() public view {
|
||||
|
||||
bytes memory code = hex"030006020000000A07";
|
||||
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0,
|
||||
9,
|
||||
0
|
||||
);
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
// Run
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
assertEq(finalIntStack.length, 0, "finalIntStack should be empty");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test with an unknown token (0xF0).
|
||||
* We expect the parser to treat it as "unknown => NOOP"
|
||||
* and produce a NOOP instruction descriptor.
|
||||
* We'll confirm finalIntStack remains empty (or unchanged).
|
||||
*/
|
||||
function test_UnknownToken() public view {
|
||||
// We'll create a 5-byte code:
|
||||
// 0x03 => SUBLIST
|
||||
// 0x00 0x01 => length=1
|
||||
// 0xF0 => unknown token
|
||||
bytes memory code = hex"030001F0";
|
||||
|
||||
// We'll parse offset=0, length=4
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0, // offset
|
||||
4, // length
|
||||
0
|
||||
);
|
||||
|
||||
// init stacks
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// finalIntStack should remain empty => no literal was pushed
|
||||
assertEq(finalIntStack.length, 0, "Expected finalIntStack to remain empty with unknown token => NOOP");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test a partial literal: we claim to have 4 bytes, but the code array ends early.
|
||||
* This should trigger the `else { break; }` branch in parseSublist
|
||||
* when trying to read 4 bytes for the INT_LITERAL.
|
||||
*/
|
||||
function test_OutOfRangeLiteral() public view {
|
||||
// 0x03 => SUBLIST
|
||||
// 0x00 0x04 => length=4
|
||||
// 0x02 => INT_LITERAL token
|
||||
// Then we have only 1 byte left instead of 4 => insufficient
|
||||
//
|
||||
// total = 1 + 2 + 1 + 1 = 5 bytes
|
||||
// The parser tries to read 4 bytes after seeing 0x04, fails, hits break.
|
||||
bytes memory code = hex"03000402FF";
|
||||
|
||||
// parse offset=0, length=5
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0,
|
||||
5,
|
||||
0
|
||||
);
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// Because the parse breaks early, we expect finalIntStack is empty or unchanged
|
||||
assertEq(finalIntStack.length, 0, "Expected parse to break on out-of-range literal => no ints pushed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test "not enough int args" scenario:
|
||||
* We'll push only 1 literal, then do INTEGER_PLUS which needs 2 integers on the stack.
|
||||
* In the main loop, it sees intTop < 2 => it does nothing.
|
||||
*/
|
||||
function test_NotEnoughIntArgs() public view {
|
||||
bytes memory code = hex"030006020000000305";
|
||||
// Breaking it down:
|
||||
// 0x03 => SUBLIST
|
||||
// 0x00 0x06 => length=6
|
||||
// 0x02 00000003 => INT_LITERAL(3)
|
||||
// 0x05 => INTEGER_MUL
|
||||
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0,
|
||||
9,
|
||||
0
|
||||
);
|
||||
|
||||
// set up stacks
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// We expect finalIntStack => [5],
|
||||
// because the PLUS instruction sees only 1 int => does nothing => 5 remains
|
||||
assertEq(finalIntStack.length, 1, "Should have 1 int left");
|
||||
assertEq(finalIntStack[0], 3, "Expected stack top=3 after insufficient-args plus");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test sublist length overflow:
|
||||
* subLen claims bigger than the code array. Should trigger else { break; }
|
||||
* in the sublist parsing for that sublist.
|
||||
*/
|
||||
function test_SublistLengthOverflow() public view {
|
||||
|
||||
bytes memory code = hex"050010FFFFFF";
|
||||
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0,
|
||||
6,
|
||||
0
|
||||
);
|
||||
|
||||
// init
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
(
|
||||
,
|
||||
,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// Expect no items pushed => finalIntStack empty
|
||||
assertEq(finalIntStack.length, 0, "Expected empty stack if sublist length is out of range => parse break");
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {Push3Interpreter} from "../src/Push3Interpreter.sol";
|
||||
|
||||
/**
|
||||
* @title Push3InterpreterTest
|
||||
* @dev Foundry test contract for the updated Push3Interpreter
|
||||
*/
|
||||
contract Push3InterpreterTest is Test {
|
||||
Push3Interpreter public interpreter;
|
||||
|
||||
function setUp() public {
|
||||
interpreter = new Push3Interpreter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test a small program that:
|
||||
* 1) Pushes literal 10 (int32)
|
||||
* 2) Pushes literal 32 (int32)
|
||||
* 3) Calls INTEGER_PLUS
|
||||
*
|
||||
* We'll store everything in a single SUBLIST:
|
||||
* 0x03 => SUBLIST
|
||||
* 0x00 0x0B => subLen=11
|
||||
* Then the next 11 bytes are:
|
||||
* 0x02 + (00 00 00 0A) => INT_LITERAL(10)
|
||||
* 0x02 + (00 00 00 20) => INT_LITERAL(32)
|
||||
* 0x01 => INTEGER_PLUS
|
||||
*
|
||||
* So total = 1 + 2 + (1+4 + 1+4 + 1) = 14 bytes
|
||||
* => 0x03 00 0B 02 00 00 00 0A 02 00 00 00 20 01
|
||||
*/
|
||||
function test_LiteralsAndPlus() public {
|
||||
// The final code array (14 bytes):
|
||||
// [0] = 0x03
|
||||
// [1..2] = 0x000B
|
||||
// [3] = 0x02
|
||||
// [4..7] = 0x0000000A => 10
|
||||
// [8] = 0x02
|
||||
// [9..12] = 0x00000020 => 32
|
||||
// [13] = 0x01 => plus
|
||||
|
||||
bytes memory code = hex"03000B020000000A020000002001";
|
||||
|
||||
// Or fully concatenated:
|
||||
// 0x03 00 0B 02 00 00 00 0A 02 00 00 00 20 01
|
||||
|
||||
// We'll parse offset=0, length=14
|
||||
uint256 sublistDesc = interpreter.makeDescriptor(
|
||||
Push3Interpreter.CodeTag.SUBLIST,
|
||||
0, // offset
|
||||
14, // length
|
||||
0
|
||||
);
|
||||
|
||||
uint256[] memory initCodeStack = new uint256[](0);
|
||||
uint256[] memory initExecStack = new uint256[](1);
|
||||
initExecStack[0] = sublistDesc;
|
||||
|
||||
int256[] memory initIntStack = new int256[](0);
|
||||
|
||||
// Run it
|
||||
(
|
||||
uint256[] memory finalCodeStack,
|
||||
uint256[] memory finalExecStack,
|
||||
int256[] memory finalIntStack
|
||||
) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack);
|
||||
|
||||
// Should produce finalIntStack = [42]
|
||||
assertEq(finalIntStack.length, 1, "finalIntStack should have length 1");
|
||||
assertEq(finalIntStack[0], 42, "Should be 10+32=42");
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,202 @@
|
||||
# SoLUSH: Running Intelligence On-Chain
|
||||
# Sovraigns: Verifiable Optimization for DeFi Protocols
|
||||
|
||||
In decentralized finance, systems often involve tasks such as determining optimal liquidity ranges for automated market makers, allocating staking rewards in a way that balances fairness and participation incentives, or dynamically adjusting collateralization ratios to minimize risks in lending protocols. These challenges are critical for ensuring the efficiency and stability of protocols, yet they are particularly difficult to address without centralization. Off-chain solutions often rely on external computation and centralized entities or oracles, introducing trust dependencies and undermining the decentralized ethos of blockchain systems.
|
||||
## The Problem
|
||||
|
||||
Artificial intelligence is increasingly being explored in decentralized finance to enhance token ecosystems and liquidity management. This shift represents a practical application of AI tools aimed at addressing complex optimization problems directly within blockchain ecosystems. While large language models (LLMs) dominate discussions around AI innovation due to their general-purpose capabilities, they are poorly suited to the demands of decentralized adaptive systems. In contrast, **genetic algorithms** (GAs) offer a more natural and efficient fit for systems requiring iterative improvements, combining computational simplicity with adaptive behavior.
|
||||
DeFi protocols lose approximately $1.5B annually through inefficient parameter settings. Uniswap V3 liquidity providers lose ~$400M from suboptimal position ranges. Lending protocols forfeit ~$300M by maintaining overly conservative collateral ratios. MEV extraction costs protocols ~$800M due to predictable parameter updates.
|
||||
|
||||
## **Introduction to Genetic Algorithms**
|
||||
Protocols need optimization algorithms, but current approaches require trust:
|
||||
|
||||
Genetic algorithms (GAs) are optimization techniques inspired by the process of natural selection. They operate by evolving a population of candidate solutions over multiple generations, using mechanisms such as mutation, crossover, and selection.
|
||||
- External consultants (Gauntlet, Chaos Labs) operate as black boxes with enterprise-only access
|
||||
- AI services require sharing sensitive protocol data with third parties
|
||||
- On-chain execution is prohibitively expensive - a simple neural network would cost millions in gas
|
||||
|
||||
- **Population:** A set of potential solutions to a problem.
|
||||
- **Fitness Function:** A way to evaluate how well each solution performs.
|
||||
- **Selection:** The process of choosing the best-performing solutions to carry forward.
|
||||
- **Crossover and Mutation:** Methods to combine and modify solutions, creating new ones that explore the problem space.
|
||||
Protocols must choose between inefficiency or centralization.
|
||||
|
||||
Through repeated iterations, GAs converge toward optimal or near-optimal solutions, making them highly effective for solving complex, dynamic problems. GAs have recently gained momentum with the development of the [Push 3 language](http://faculty.hampshire.edu/lspector/push3-description.html) by Lee Spector. Push 3’s compact and efficient architecture shares similarities with stack-based virtual machine systems like the Ethereum Virtual Machine (EVM). Although not designed specifically for EVMs, Push 3’s simplicity and flexibility make it conceptually compatible with blockchain execution environments. Push 3 has achieved milestones such as evolving [programs for quantum computers](https://faculty.hampshire.edu/lspector/aqcp/) and supporting autoconstructive evolution. A Push 3 VM could facilitate the deployment of genetic algorithms in decentralized systems, leveraging Push 3’s support for expressive program evolution and adaptive behavior in tasks like resource optimization.
|
||||
## Our Approach
|
||||
|
||||
## On Agents
|
||||
Sovraigns enables verifiable optimization through genetic algorithms, zero-knowledge proofs, and based rollups:
|
||||
|
||||
An agent is defined as an entity capable of perceiving its environment, processing information, and taking autonomous actions to achieve specific objectives. LLMs can be structured into agents using frameworks that provide memory and feedback loops, but they do not fit on-chain and, at the same time, cannot securely manage secrets like private keys. Without either of these capabilities, LLM-based agents require external key management, increasing risks and undermining decentralized security principles. In contrast, genetic algorithms exhibit agentic characteristics by evolving their behavior over time to meet specific goals. When deployed on-chain within smart contracts, GAs can autonomously adapt to real-time inputs and optimize decisions, all while operating entirely within the secure and deterministic framework of blockchain systems.
|
||||
1. Algorithms evolve off-chain through competition between optimizers
|
||||
2. Zero-knowledge proofs verify correct execution without revealing the algorithm
|
||||
3. A based rollup coordinates requests and stores proven results
|
||||
|
||||
---
|
||||
Protocols receive optimized parameters with cryptographic proof of correctness, without trusting any party.
|
||||
|
||||
## **Genetic Algorithms vs. LLMs for Decentralized Systems**
|
||||
## Market Size
|
||||
|
||||
1. **Efficiency and Feasibility for Blockchain Execution**
|
||||
Current DeFi inefficiencies:
|
||||
|
||||
- **LLMs are resource-intensive:** The computational complexity and size of LLMs make them impractical for direct execution in decentralized environments, where every operation incurs costs. Running even the simplest inference for an LLM would require off-chain computation and trust in external oracles, undermining decentralization.
|
||||
- **GAs are lightweight:** Genetic algorithms, by design, are computationally efficient. Their iterative, population-based approach to optimization fits naturally into resource-constrained environments. This efficiency ensures GAs can evolve and execute entirely on-chain, preserving transparency and trustless execution.
|
||||
| **DeFi Inefficiency** | **Annual Value Lost** | **Root Cause** |
|
||||
|---|---|---|
|
||||
| Uniswap V3 LP Losses | ~$400M | Suboptimal position management |
|
||||
| Lending Protocol Inefficiency | ~$300M | Conservative parameters |
|
||||
| MEV/Arbitrage Extraction | ~$800M | Predictable protocol behavior |
|
||||
| **Total Market** | **$1.5B+** | **Lack of intelligent optimization** |
|
||||
|
||||
2. **Evolution vs. Training**
|
||||
With DeFi TVL at $50B, we estimate the optimization market at $2-3B annually today, potentially reaching $10B as protocols become more complex.
|
||||
|
||||
- **GAs evolve in parallel:** Genetic algorithms enable evolutionary processes to run across distributed populations simultaneously. This parallelism contrasts with the centralized training required for deep neural networks, which demand synchronized computation and significant infrastructure. GAs’ decentralized nature aligns well with blockchain environments.
|
||||
### Current Solutions vs. Sovraigns
|
||||
|
||||
- **Deep neural networks require centralized training:** Neural networks demand substantial computational resources and centralized coordination to train effectively.
|
||||
| **Aspect** | **Current Solutions** | **Sovraigns** |
|
||||
|---|---|---|
|
||||
| **Trust Model** | Centralized (Gauntlet, Chaos Labs) | Trustless ZK proofs |
|
||||
| **Accessibility** | Enterprise contracts only | Permissionless for all protocols |
|
||||
| **Transparency** | Black box optimization | Fully auditable algorithms |
|
||||
| **Cost** | High consulting fees | Pay-per-optimization |
|
||||
| **Adaptability** | Manual updates | Continuous evolution |
|
||||
| **Security** | Single point of failure | Decentralized validation |
|
||||
|
||||
3. **Auditability of Evolution vs Training**
|
||||
|
||||
- **Training LLMs is opaque:** The progress of training deep neural networks is often difficult to evaluate and verify because testing its capabilities relies on secret data, which cannot be audited by external parties.
|
||||
- **GAs offer transparent evolution:** Genetic algorithms allow the progress of evolution to be easily audited against a target fitness function. This process is fully observable and can be verified at each step with minimal effort.
|
||||
## Why Genetic Algorithms Over LLMs
|
||||
|
||||
---
|
||||
Technical comparison for decentralized systems:
|
||||
|
||||
## **Theoretical Framework for Decentralized Algorithm Evolution**
|
||||
| **Aspect** | **LLMs** | **Genetic Algorithms** |
|
||||
|---|---|---|
|
||||
| **Computational Requirements** | GPUs, massive memory, centralized inference | Lightweight, runs anywhere |
|
||||
| **Blockchain Compatibility** | Impossible on-chain, requires external APIs | Efficient ZK-proof verification |
|
||||
| **Trust Model** | Black box, external dependencies | Fully verifiable with cryptographic proofs |
|
||||
| **Cost** | $1000s per complex optimization | Pennies per optimization |
|
||||
| **Adaptability** | Fixed after training | Continuously evolves to market conditions |
|
||||
| **Determinism** | Non-deterministic, unreliable | Deterministic, predictable outputs |
|
||||
| **Decentralization** | Requires centralized providers | Fully decentralized execution |
|
||||
|
||||
A protocol providing agents as a service could leverage genetic algorithms to address complex problems within blockchain environments. Read through this [real-life usecase explaining the applicability of agents to DeFi](example.md). Central to such a system would be the development of a Push 3 Virtual Machine (VM), enabling efficient execution of genetic algorithms. An "Optimizer Pool", an network of nodes could collaboratively train and evolve these algorithms offchain. These nodes would compete to improve the fitness of the algorithms. Once an improvement, a measuruable better solution to the fitness function is found in off-chain computation, a node would submit it to be accepted into the population of solutions registered on chain, in exchange for a rewards payment by the protocol. The nodes of the optimizer pool would run under an incentivization model simira to proof-of-work systems.
|
||||
Genetic algorithms provide the optimization capabilities of LLMs while remaining verifiable, deterministic, and cost-effective for blockchain environments.
|
||||
|
||||

|
||||
## Technical Architecture
|
||||
|
||||
Three core components enable verifiable optimization on Ethereum:
|
||||
|
||||
1. **Based Rollup Coordination**
|
||||
- Manages algorithm registry and optimization requests
|
||||
- Batches proofs for cost-effective verification
|
||||
- Inherits Ethereum's security model
|
||||
|
||||
2. **OpenVM Execution Environment**
|
||||
- Runs genetic algorithms in a provable VM
|
||||
- Generates STARK proofs for each optimization
|
||||
- Ensures deterministic execution
|
||||
|
||||
3. **Optimizer Network**
|
||||
- Independent nodes compete to evolve better algorithms
|
||||
- Council validates quality before on-chain registration
|
||||
- Economic incentives tied to algorithm performance
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
The protocol operates across three layers:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ETHEREUM L1 │
|
||||
│ • Final Settlement • Proof Verification • Protocol Updates │
|
||||
└────────────────────────────┬───────────────────────────────────┘
|
||||
│ State Bridge
|
||||
┌────────────────────────────┴───────────────────────────────────┐
|
||||
│ BASED ROLLUP (Sidechain) │
|
||||
│ • Task Registry • Algorithm Storage • Coordination │
|
||||
│ • Reward Pools • Version History • Batch Requests │
|
||||
└────────────────────────────┬───────────────────────────────────┘
|
||||
│ ZK Proofs
|
||||
┌────────────────────────────┴───────────────────────────────────┐
|
||||
│ OFF-CHAIN COMPUTE │
|
||||
│ • Algorithm Evolution • OpenVM Execution • Proof Generation │
|
||||
│ • Council Validation • Optimizer Competition │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| **Layer** | **Purpose** | **Key Components** |
|
||||
|---|---|---|
|
||||
| **Ethereum L1** | Final settlement and security | Smart contracts, State verification |
|
||||
| **Based Rollup** | Decentralized coordination | Task registry, Algorithm storage |
|
||||
| **Off-Chain** | Computation and evolution | OpenVM, Genetic algorithms, ZK proofs |
|
||||
|
||||
### Based Rollup: Decentralized Coordination
|
||||
1. **Task Registration**: DeFi protocols register optimization tasks on the based rollup, including:
|
||||
- Fitness functions (optimization goals)
|
||||
- Evaluation parameters and constraints
|
||||
- Reward pools for successful optimizations
|
||||
|
||||
2. **Algorithm Registry**: The best algorithms for each task are registered on-chain after council validation
|
||||
- Algorithm hashes stored on-chain for verification
|
||||
- ZK proofs verify the exact algorithm (matching hash) was executed
|
||||
- Version history maintained for auditability
|
||||
- Permissionless access for all protocols
|
||||
|
||||
3. **Execution Requests**: When protocols need optimization:
|
||||
- Send request to based rollup with current data
|
||||
- Request triggers off-chain execution
|
||||
- Multiple requests batched for efficiency
|
||||
|
||||
### Off-Chain: Evolution and Zero-Knowledge Execution
|
||||
1. **Algorithm Evolution**:
|
||||
- Optimizers run genetic algorithms using standard Push 3 implementations
|
||||
- Evolution happens on local high-performance infrastructure
|
||||
- Best solutions submitted to council for validation
|
||||
- No ZK proofs needed during evolution phase
|
||||
|
||||
2. **Zero-Knowledge Proof Generation** (triggered by on-chain requests):
|
||||
- Registered algorithms execute in OpenVM with request data
|
||||
- OpenVM verifies the algorithm matches the on-chain hash
|
||||
- STARK proofs generated via Plonky3
|
||||
- Proofs include complete execution trace and results
|
||||
- Multiple proofs batched into rollup blocks
|
||||
|
||||
3. **Council Validation**:
|
||||
- Distributed validators verify algorithm improvements
|
||||
- Consensus required before on-chain registration
|
||||
- Ensures quality without centralized control
|
||||
|
||||
### L1 Integration: Trustless Protocol Updates
|
||||
1. **State Bridge**: Based rollup posts state roots to Ethereum
|
||||
2. **Proof Verification**: Smart contracts verify STARK proofs
|
||||
3. **Action Execution**: DeFi protocols execute optimizations based on verified results
|
||||
|
||||
### Key Components
|
||||
- **Registry (based rollup)**: Task and algorithm management with ZK verification
|
||||
- **OpenVM Runtime**: Custom Push 3 implementation for genetic programming
|
||||
- **Optimizers (off-chain)**: Compete to evolve better solutions
|
||||
- **Council (distributed)**: Validates algorithm quality before registration
|
||||
- **Proof Aggregator**: Batches multiple ZK proofs for efficient verification
|
||||
- **State Bridge**: Synchronizes rollup state with Ethereum L1
|
||||
|
||||
## Timing
|
||||
|
||||
Three technical developments make this feasible now:
|
||||
|
||||
1. **ZK Infrastructure**
|
||||
- STARK proving systems now production-ready
|
||||
- Proof generation costs decreased 100x since 2022
|
||||
- OpenVM provides suitable execution environment
|
||||
|
||||
2. **Based Rollups**
|
||||
- Ethereum's rollup infrastructure mature enough for production
|
||||
- Provides coordination without sacrificing decentralization
|
||||
- Cost structure makes continuous optimization viable
|
||||
|
||||
3. **Market Demand**
|
||||
- Protocols reaching limits of manual parameter tuning
|
||||
- Increased scrutiny requires auditable optimization
|
||||
- Competition driving need for efficiency gains
|
||||
|
||||
## Competitive Position
|
||||
|
||||
**Technical Differentiation:**
|
||||
- First implementation of ZK-verified genetic algorithms for DeFi
|
||||
- Custom VM optimized for this use case
|
||||
- Integration with Ethereum's rollup infrastructure
|
||||
|
||||
**Network Effects:**
|
||||
- More protocols using the system improves algorithm quality
|
||||
- Optimizer competition creates continuous improvement
|
||||
- Registry accumulates valuable optimization history
|
||||
|
||||
**Barriers to Entry:**
|
||||
- Requires expertise in genetic programming, zero-knowledge systems, and DeFi mechanics
|
||||
- Complex coordination between multiple system layers
|
||||
- Significant development time to reach production quality
|
||||
|
||||
We aim to become the standard optimization layer for DeFi protocols, addressing a multi-billion dollar inefficiency.
|
||||
|
||||
To evaluate the effectiveness of distributed evolution, a "Council" mechanism needs to be created. This Council would consist of validators who execute the latest evolved algorithms and assess their performance against a predefined target fitness function. Validators would then cast votes on whether the new algorithms deliver measurable improvements. This transparent voting process ensures that only the most effective solutions are rewarded and integrated, fostering trust and collaboration across the Optimizer Pool. By aligning incentives with measurable progress, such a framework provides a reliable and decentralized method for advancing algorithmic evolution.
|
||||
|
||||
Ultimately, this system creates a foundation for scalable agent development, aligning with the broader concept of agents as a service, where users and protocols in DeFi can deploy and leverage optimized, evolving algorithms tailored to specific tasks.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user