fix(ci): Use GitHub ref for setup-java.
This commit is contained in:
parent
1ce3286451
commit
4f68bc964b
8 changed files with 250 additions and 26 deletions
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-java@v4
|
- uses: https://github.com/actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "25"
|
java-version: "25"
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ enum class OpCode(val code: Byte) {
|
||||||
data class Instruction(val opCode: OpCode, val immediate: ByteArray = byteArrayOf()) {
|
data class Instruction(val opCode: OpCode, val immediate: ByteArray = byteArrayOf()) {
|
||||||
init {
|
init {
|
||||||
require(opCode != OpCode.PUSH32 || immediate.size == 32)
|
require(opCode != OpCode.PUSH32 || immediate.size == 32)
|
||||||
|
require(opCode == OpCode.PUSH32 || immediate.isEmpty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,8 +79,10 @@ object TwistBytecode {
|
||||||
while (input.hasRemaining()) {
|
while (input.hasRemaining()) {
|
||||||
val code = input.get()
|
val code = input.get()
|
||||||
val op = OpCode.entries.firstOrNull { it.code == code } ?: error("Unknown opcode")
|
val op = OpCode.entries.firstOrNull { it.code == code } ?: error("Unknown opcode")
|
||||||
val immediate =
|
val immediate = if (op == OpCode.PUSH32) {
|
||||||
if (op == OpCode.PUSH32) ByteArray(32).also(input::get) else byteArrayOf()
|
require(input.remaining() >= 32) { "Truncated PUSH32 immediate" }
|
||||||
|
ByteArray(32).also(input::get)
|
||||||
|
} else byteArrayOf()
|
||||||
output += Instruction(op, immediate)
|
output += Instruction(op, immediate)
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,49 @@ data class ContractIr(
|
||||||
val functions: List<FunctionIr>,
|
val functions: List<FunctionIr>,
|
||||||
val accessPolicy: IrAccessPolicy = IrAccessPolicy(),
|
val accessPolicy: IrAccessPolicy = IrAccessPolicy(),
|
||||||
) {
|
) {
|
||||||
fun bytecode(): ByteArray = TwistBytecode.encode(functions.flatMap { it.instructions })
|
/**
|
||||||
|
* Encodes a deployed contract. Calldata word zero is the function selector and all
|
||||||
|
* function arguments begin at word one. A non-matching selector deterministically reverts.
|
||||||
|
*/
|
||||||
|
fun bytecode(): ByteArray {
|
||||||
|
require(functions.map { it.selector }.distinct().size == functions.size) { "Duplicate function selector" }
|
||||||
|
val dispatchSize = 2 + functions.size * 5 + 1
|
||||||
|
var nextBody = dispatchSize
|
||||||
|
val targets = functions.associate { function ->
|
||||||
|
function.selector to nextBody.also { nextBody += function.instructions.size }
|
||||||
|
}
|
||||||
|
val output = mutableListOf(
|
||||||
|
Instruction(OpCode.PUSH32, Word256.ZERO.toBytes()),
|
||||||
|
Instruction(OpCode.CALLDATA_LOAD),
|
||||||
|
)
|
||||||
|
functions.forEach { function ->
|
||||||
|
output += Instruction(OpCode.DUP)
|
||||||
|
output += Instruction(OpCode.PUSH32, Word256.fromLong(function.selector.toLong()).toBytes())
|
||||||
|
output += Instruction(OpCode.EQ)
|
||||||
|
output += Instruction(OpCode.PUSH32, Word256.fromLong(targets.getValue(function.selector).toLong()).toBytes())
|
||||||
|
output += Instruction(OpCode.JUMPI)
|
||||||
|
}
|
||||||
|
output += Instruction(OpCode.REVERT)
|
||||||
|
functions.forEach { function ->
|
||||||
|
val offset = targets.getValue(function.selector)
|
||||||
|
output += function.instructions.mapIndexed { index, instruction ->
|
||||||
|
if (
|
||||||
|
instruction.opCode == OpCode.PUSH32 &&
|
||||||
|
function.instructions.getOrNull(index + 1)?.opCode in setOf(OpCode.JUMP, OpCode.JUMPI)
|
||||||
|
) {
|
||||||
|
Instruction(
|
||||||
|
OpCode.PUSH32,
|
||||||
|
Word256.of(
|
||||||
|
Word256.fromBytes(instruction.immediate).toBigInteger() +
|
||||||
|
java.math.BigInteger.valueOf(offset.toLong())
|
||||||
|
)
|
||||||
|
.toBytes(),
|
||||||
|
)
|
||||||
|
} else instruction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TwistBytecode.encode(output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FunctionIr(val name: String, val selector: Int, val instructions: List<Instruction>)
|
data class FunctionIr(val name: String, val selector: Int, val instructions: List<Instruction>)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package rip.crit.twist.compiler
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import rip.crit.twist.bytecode.Instruction
|
import rip.crit.twist.bytecode.Instruction
|
||||||
import rip.crit.twist.bytecode.OpCode
|
import rip.crit.twist.bytecode.OpCode
|
||||||
|
import rip.crit.twist.jit.LLVMBuilder
|
||||||
import rip.crit.twist.tvm.Word256
|
import rip.crit.twist.tvm.Word256
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,6 +24,108 @@ class TwistMlCompiler {
|
||||||
FunctionIr(function.name, index + 1, Lowerer(function.parameters).lower(function.body))
|
FunctionIr(function.name, index + 1, Lowerer(function.parameters).lower(function.body))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Lowers pure arithmetic and explicit storage effects to portable LLVM IR. */
|
||||||
|
fun lowerToLlvm(source: String, moduleName: String = "twistml"): LLVMBuilder {
|
||||||
|
val contract = parse(source)
|
||||||
|
return LLVMBuilder(moduleName, LlvmLowerer(contract).lower())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LlvmLowerer(private val contract: TwistMlContract) {
|
||||||
|
private var value = 0
|
||||||
|
private var label = 0
|
||||||
|
private val lines = mutableListOf<String>()
|
||||||
|
private var needsLoad = false
|
||||||
|
private var needsStore = false
|
||||||
|
private var needsCaller = false
|
||||||
|
private var needsCallValue = false
|
||||||
|
|
||||||
|
fun lower(): String {
|
||||||
|
contract.functions.forEach { function -> lowerFunction(function) }
|
||||||
|
return buildString {
|
||||||
|
append("target triple = \"unknown-unknown-unknown\"\n")
|
||||||
|
if (needsLoad) append("declare i256 @twist_sload(i256)\n")
|
||||||
|
if (needsStore) append("declare void @twist_sstore(i256, i256)\n")
|
||||||
|
if (needsCaller) append("declare i256 @twist_caller()\n")
|
||||||
|
if (needsCallValue) append("declare i256 @twist_callvalue()\n")
|
||||||
|
append(lines.joinToString("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lowerFunction(function: TwistMlFunction) {
|
||||||
|
value = 0
|
||||||
|
label = 0
|
||||||
|
val arguments = function.parameters.mapIndexed { index, _ -> "i256 %arg$index" }
|
||||||
|
lines += "define i256 @${function.name}(" + arguments.joinToString(", ") + ") {"
|
||||||
|
lines += "entry:"
|
||||||
|
val environment = function.parameters.mapIndexed { index, name -> name to "%arg$index" }.toMap()
|
||||||
|
val result = emit(function.body, environment)
|
||||||
|
lines += " ret i256 $result"
|
||||||
|
lines += "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emit(expression: TwistMlExpr, environment: Map<String, String>): String = when (expression) {
|
||||||
|
is TwistMlExpr.Number -> expression.value.toString()
|
||||||
|
is TwistMlExpr.Variable -> environment[expression.name] ?: error("Unknown variable ${expression.name}")
|
||||||
|
is TwistMlExpr.Let -> emit(expression.body, environment + (expression.name to emit(expression.value, environment)))
|
||||||
|
TwistMlExpr.Caller -> {
|
||||||
|
needsCaller = true
|
||||||
|
call("i256 @twist_caller()")
|
||||||
|
}
|
||||||
|
TwistMlExpr.CallValue -> {
|
||||||
|
needsCallValue = true
|
||||||
|
call("i256 @twist_callvalue()")
|
||||||
|
}
|
||||||
|
is TwistMlExpr.Load -> {
|
||||||
|
needsLoad = true
|
||||||
|
call("i256 @twist_sload(${emit(expression.key, environment)})")
|
||||||
|
}
|
||||||
|
is TwistMlExpr.Store -> {
|
||||||
|
needsStore = true
|
||||||
|
lines += " call void @twist_sstore(i256 ${emit(expression.key, environment)}, i256 ${emit(expression.value, environment)})"
|
||||||
|
"0"
|
||||||
|
}
|
||||||
|
is TwistMlExpr.Binary -> {
|
||||||
|
val left = emit(expression.left, environment)
|
||||||
|
val right = emit(expression.right, environment)
|
||||||
|
when (expression.operator) {
|
||||||
|
"+" -> instruction("add i256 $left, $right")
|
||||||
|
"-" -> instruction("sub i256 $left, $right")
|
||||||
|
"*" -> instruction("mul i256 $left, $right")
|
||||||
|
"/" -> instruction("udiv i256 $left, $right")
|
||||||
|
"%" -> instruction("urem i256 $left, $right")
|
||||||
|
"&&" -> instruction("and i256 $left, $right")
|
||||||
|
"||" -> instruction("or i256 $left, $right")
|
||||||
|
"=", "==", "<", ">" -> {
|
||||||
|
val predicate = mapOf("=" to "eq", "==" to "eq", "<" to "ult", ">" to "ugt").getValue(expression.operator)
|
||||||
|
val comparison = instruction("icmp $predicate i256 $left, $right")
|
||||||
|
instruction("zext i1 $comparison to i256")
|
||||||
|
}
|
||||||
|
else -> error("Unsupported operator ${expression.operator}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TwistMlExpr.If -> {
|
||||||
|
val condition = emit(expression.condition, environment)
|
||||||
|
val test = instruction("icmp ne i256 $condition, 0")
|
||||||
|
val id = label++
|
||||||
|
val thenLabel = "then$id"
|
||||||
|
val elseLabel = "else$id"
|
||||||
|
val mergeLabel = "merge$id"
|
||||||
|
lines += " br i1 $test, label %$thenLabel, label %$elseLabel"
|
||||||
|
lines += "$thenLabel:"
|
||||||
|
val thenValue = emit(expression.thenBranch, environment)
|
||||||
|
lines += " br label %$mergeLabel"
|
||||||
|
lines += "$elseLabel:"
|
||||||
|
val elseValue = emit(expression.elseBranch, environment)
|
||||||
|
lines += " br label %$mergeLabel"
|
||||||
|
lines += "$mergeLabel:"
|
||||||
|
instruction("phi i256 [$thenValue, %$thenLabel], [$elseValue, %$elseLabel]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun call(body: String): String = instruction("call $body")
|
||||||
|
private fun instruction(body: String): String = "%v${value++}".also { lines += " $it = $body" }
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TwistMlContract(val name: String, val functions: List<TwistMlFunction>)
|
data class TwistMlContract(val name: String, val functions: List<TwistMlFunction>)
|
||||||
|
|
@ -43,7 +146,7 @@ sealed interface TwistMlExpr {
|
||||||
|
|
||||||
private class Lowerer(parameters: List<String>) {
|
private class Lowerer(parameters: List<String>) {
|
||||||
private val parameterValues = parameters.mapIndexed { index, name ->
|
private val parameterValues = parameters.mapIndexed { index, name ->
|
||||||
name to listOf(Instruction(OpCode.PUSH32, Word256.fromLong((index * 32).toLong()).toBytes()), Instruction(OpCode.CALLDATA_LOAD))
|
name to listOf(Instruction(OpCode.PUSH32, Word256.fromLong(((index + 1) * 32).toLong()).toBytes()), Instruction(OpCode.CALLDATA_LOAD))
|
||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
fun lower(expression: TwistMlExpr): List<Instruction> = expression.lower(parameterValues) + Instruction(OpCode.RETURN)
|
fun lower(expression: TwistMlExpr): List<Instruction> = expression.lower(parameterValues) + Instruction(OpCode.RETURN)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ import rip.crit.twist.tvm.Calldata
|
||||||
import rip.crit.twist.tvm.TransientStorage
|
import rip.crit.twist.tvm.TransientStorage
|
||||||
import rip.crit.twist.tvm.Word256
|
import rip.crit.twist.tvm.Word256
|
||||||
import rip.crit.twist.tvm.BytecodeVirtualMachine
|
import rip.crit.twist.tvm.BytecodeVirtualMachine
|
||||||
|
import rip.crit.twist.tvm.ExecutionContext
|
||||||
|
import rip.crit.twist.tvm.StateExecutionHost
|
||||||
import rip.crit.twist.bytecode.Instruction
|
import rip.crit.twist.bytecode.Instruction
|
||||||
import rip.crit.twist.bytecode.OpCode
|
import rip.crit.twist.bytecode.OpCode
|
||||||
import rip.crit.twist.p2p.PeerNetwork
|
import rip.crit.twist.p2p.PeerNetwork
|
||||||
|
|
@ -394,12 +396,36 @@ class AppTest {
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val contract = TwistMlCompiler().compile(source)
|
val contract = TwistMlCompiler().compile(source)
|
||||||
assertEquals(listOf("max", "stored"), contract.functions.map { it.name })
|
assertEquals(listOf("max", "stored"), contract.functions.map { it.name })
|
||||||
val input = Word256.fromLong(3).toBytes() + Word256.fromLong(7).toBytes()
|
val input =
|
||||||
val result = BytecodeVirtualMachine().execute(contract.functions.first().instructions.let(TwistBytecode::encode), input, Address("ml"))
|
Word256.fromLong(1).toBytes() +
|
||||||
|
Word256.fromLong(3).toBytes() +
|
||||||
|
Word256.fromLong(7).toBytes()
|
||||||
|
val result = BytecodeVirtualMachine().execute(contract.bytecode(), input, Address("ml"))
|
||||||
assertTrue(result.success)
|
assertTrue(result.success)
|
||||||
assertEquals(BigInteger.valueOf(7), Word256.fromBytes(result.output).toBigInteger())
|
assertEquals(BigInteger.valueOf(7), Word256.fromBytes(result.output).toBigInteger())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun twistoTwistMlContractInitializesAndTransfersOnTheVm() {
|
||||||
|
val contract = TwistMlCompiler().compile(Files.readString(Path.of("protocols/tokens/twisto.twml")))
|
||||||
|
val code = contract.bytecode()
|
||||||
|
val state = InMemoryWorldState()
|
||||||
|
val address = Address("twisto")
|
||||||
|
val owner = Address("owner")
|
||||||
|
fun call(caller: Address, selector: Long, vararg arguments: Word256) =
|
||||||
|
BytecodeVirtualMachine().execute(
|
||||||
|
code,
|
||||||
|
Word256.fromLong(selector).toBytes() + arguments.fold(byteArrayOf()) { bytes, word -> bytes + word.toBytes() },
|
||||||
|
StateExecutionHost(ExecutionContext(address, caller, Amount(BigInteger.ZERO)), state),
|
||||||
|
)
|
||||||
|
assertTrue(call(owner, 1, Word256.fromLong(99), Word256.fromLong(10), Word256.fromLong(10)).success)
|
||||||
|
val recipient = Word256.fromBytes(Sha256.bytes("recipient".encodeToByteArray()))
|
||||||
|
assertTrue(call(owner, 6, recipient, Word256.fromLong(4)).success)
|
||||||
|
val ownerKey = Word256.fromBytes(Sha256.bytes(owner.value.encodeToByteArray()))
|
||||||
|
assertEquals(BigInteger.valueOf(6), Word256.fromBytes(call(owner, 5, ownerKey).output).toBigInteger())
|
||||||
|
assertEquals(BigInteger.valueOf(4), Word256.fromBytes(call(owner, 5, recipient).output).toBigInteger())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun didMethodsResolveDocuments() {
|
fun didMethodsResolveDocuments() {
|
||||||
val publicKey = ByteArray(32) { it.toByte() }
|
val publicKey = ByteArray(32) { it.toByte() }
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
package rip.crit.twist.tool.twisto
|
package rip.crit.twist.tool.twisto
|
||||||
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import rip.crit.twist.bips.FileStorageContract
|
import rip.crit.twist.bips.FileStorageContract
|
||||||
import rip.crit.twist.core.Address
|
import rip.crit.twist.compiler.TwistMlCompiler
|
||||||
import rip.crit.twist.core.Amount
|
|
||||||
import rip.crit.twist.store.FileKeyValueStore
|
|
||||||
import rip.crit.twist.twisto.TwistoMetadata
|
import rip.crit.twist.twisto.TwistoMetadata
|
||||||
import rip.crit.twist.twisto.TwistoToken
|
import rip.crit.twist.tvm.Word256
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
require(args.size in 1..3) {
|
require(args.size in 1..3) {
|
||||||
"Usage is twisto <data-folder> [owner] [initial-supply]"
|
"Usage is twisto <data-folder> [owner-label] [initial-supply]"
|
||||||
}
|
}
|
||||||
val root = Path.of(args[0])
|
val root = Path.of(args[0])
|
||||||
val owner = Address(args.getOrNull(1) ?: "twisto-owner")
|
val owner = args.getOrNull(1) ?: "twisto-owner"
|
||||||
val token =
|
val supply = BigInteger(args.getOrNull(2) ?: "1000000")
|
||||||
TwistoToken(
|
val metadata = FileStorageContract(root.resolve("metadata"))
|
||||||
owner,
|
val pointer = metadata.put(TwistoMetadata(description = "Named token on the Twist network").encode())
|
||||||
FileKeyValueStore(root.resolve("state")),
|
val source = checkNotNull(object {}.javaClass.getResource("/twisto.twml")) { "Missing Twisto contract source" }.readText()
|
||||||
FileStorageContract(root.resolve("metadata")),
|
val code = TwistMlCompiler().compile(source).bytecode()
|
||||||
)
|
Files.createDirectories(root)
|
||||||
val pointer =
|
Files.write(root.resolve("twisto.tbc"), code)
|
||||||
token.metadataPointer()
|
val initialization =
|
||||||
?: token.deploy(
|
Word256.fromLong(1).toBytes() +
|
||||||
TwistoMetadata(description = "Named token on the Twist network"),
|
Word256.fromBytes(pointer.value.chunked(2).map { it.toInt(16).toByte() }.toByteArray()).toBytes() +
|
||||||
Amount(BigInteger(args.getOrNull(2) ?: "1000000")),
|
Word256.of(supply).toBytes() +
|
||||||
)
|
Word256.of(supply).toBytes()
|
||||||
println("deployed symbol=${token.symbol} metadata=${pointer.value} owner=${owner.value}")
|
Files.write(root.resolve("initialize.calldata"), initialization)
|
||||||
|
println("compiled contract=${root.resolve("twisto.tbc")} metadata=${pointer.value} owner-label=$owner")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
apps/twisto/src/main/resources/twisto.twml
Normal file
10
apps/twisto/src/main/resources/twisto.twml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
contract Twisto {
|
||||||
|
fun initialize(metadata, supply, cap) = if load(0) == 0 then let ignored = store(0, caller) in let ignored = store(1, metadata) in let ignored = store(2, cap) in let ignored = store(3, supply) in store(caller, supply) else 0;
|
||||||
|
fun metadata() = load(1);
|
||||||
|
fun total_supply() = load(3);
|
||||||
|
fun cap() = load(2);
|
||||||
|
fun balance_of(account) = load(account);
|
||||||
|
fun transfer(to, amount) = let from = caller in let balance = load(from) in if amount > balance then 0 else let ignored = store(from, balance - amount) in store(to, load(to) + amount);
|
||||||
|
fun mint(to, amount) = let owner = load(0) in let supply = load(3) in let ceiling = load(2) in if caller == owner then if ceiling < supply + amount then 0 else let ignored = store(3, supply + amount) in store(to, load(to) + amount) else 0;
|
||||||
|
fun burn(amount) = let account = caller in let balance = load(account) in let supply = load(3) in if amount > balance then 0 else let ignored = store(account, balance - amount) in store(3, supply - amount);
|
||||||
|
}
|
||||||
40
protocols/tokens/twisto.twml
Normal file
40
protocols/tokens/twisto.twml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
contract Twisto {
|
||||||
|
fun initialize(metadata, supply, cap) =
|
||||||
|
if load(0) == 0 then
|
||||||
|
let ignored = store(0, caller) in
|
||||||
|
let ignored = store(1, metadata) in
|
||||||
|
let ignored = store(2, cap) in
|
||||||
|
let ignored = store(3, supply) in
|
||||||
|
store(caller, supply)
|
||||||
|
else 0;
|
||||||
|
|
||||||
|
fun metadata() = load(1);
|
||||||
|
fun total_supply() = load(3);
|
||||||
|
fun cap() = load(2);
|
||||||
|
fun balance_of(account) = load(account);
|
||||||
|
|
||||||
|
fun transfer(to, amount) =
|
||||||
|
let from = caller in
|
||||||
|
let balance = load(from) in
|
||||||
|
if amount > balance then 0 else
|
||||||
|
let ignored = store(from, balance - amount) in
|
||||||
|
store(to, load(to) + amount);
|
||||||
|
|
||||||
|
fun mint(to, amount) =
|
||||||
|
let owner = load(0) in
|
||||||
|
let supply = load(3) in
|
||||||
|
let ceiling = load(2) in
|
||||||
|
if caller == owner then
|
||||||
|
if ceiling < supply + amount then 0 else
|
||||||
|
let ignored = store(3, supply + amount) in
|
||||||
|
store(to, load(to) + amount)
|
||||||
|
else 0;
|
||||||
|
|
||||||
|
fun burn(amount) =
|
||||||
|
let account = caller in
|
||||||
|
let balance = load(account) in
|
||||||
|
let supply = load(3) in
|
||||||
|
if amount > balance then 0 else
|
||||||
|
let ignored = store(account, balance - amount) in
|
||||||
|
store(3, supply - amount);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue