branch(main): Initial commit.
Some checks are pending
Build and publish / verify (push) Waiting to run
Build and publish / publish (push) Blocked by required conditions

This commit is contained in:
Hellings 2026-09-12 08:25:10 -04:00
commit ef72a56501
142 changed files with 6977 additions and 0 deletions

View file

@ -0,0 +1,32 @@
name: Build and publish
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "25"
- run: ./gradlew test
publish:
if: startsWith(gitea.ref, 'refs/tags/v')
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "25"
- run: ./gradlew -Pversion="${GITHUB_REF_NAME#v}" publish
env:
ORG_GRADLE_PROJECT_forgejoUsername: ${{ secrets.FORGEJO_USERNAME }}
ORG_GRADLE_PROJECT_forgejoPassword: ${{ secrets.FORGEJO_TOKEN }}

12
.gitattributes vendored Normal file
View file

@ -0,0 +1,12 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
# Binary files should be left untouched
*.jar binary

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Ignore Gradle project-specific cache directory
.gradle/
# Ignore Gradle build output directory
build/
# Ignore Kotlin plugin data
.kotlin/

0
README.md Normal file
View file

3
app/build.gradle.kts Normal file
View file

@ -0,0 +1,3 @@
dependencies {
testImplementation(kotlin("test"))
}

View file

@ -0,0 +1,66 @@
package rip.crit.twist
import java.nio.file.Path
import java.util.concurrent.CountDownLatch
import rip.crit.twist.consensus.BasicConsensusEngine
import rip.crit.twist.consensus.ConsensusEngine
import rip.crit.twist.gossip.GossipService
import rip.crit.twist.gossip.InMemoryGossipService
import rip.crit.twist.p2p.PeerId
import rip.crit.twist.p2p.PeerNetwork
import rip.crit.twist.state.FileWorldState
import rip.crit.twist.state.WorldState
import rip.crit.twist.transport.TcpPeerNetwork
class App(
val network: PeerNetwork,
val gossip: GossipService,
val consensus: ConsensusEngine,
val state: WorldState,
) {
fun start() {
network.start()
gossip.start()
consensus.start()
}
fun stop() {
consensus.stop()
gossip.stop()
network.stop()
}
}
fun main(args: Array<String>) {
val root = Path.of(args.firstOrNull { !it.startsWith("--") } ?: "build/node")
val port = args.firstOrNull { it.startsWith("--port=") }?.substringAfter('=')?.toInt() ?: 0
val network = TcpPeerNetwork(PeerId("node-${root.fileName}"), port)
val app =
App(
network,
InMemoryGossipService(),
BasicConsensusEngine(),
FileWorldState(root.resolve("state")),
)
app.start()
args
.filter { it.startsWith("--peer=") }
.forEach { option ->
val (host, peerPort) = option.substringAfter('=').split(':', limit = 2)
network.connect(host, peerPort.toInt())
}
println("Twist node started data=$root port=${network.port}")
if ("--once" in args) {
app.stop()
return
}
val stopped = CountDownLatch(1)
Runtime.getRuntime()
.addShutdownHook(
Thread {
app.stop()
stopped.countDown()
}
)
stopped.await()
}

View file

@ -0,0 +1,9 @@
package rip.crit.twist.access
import rip.crit.twist.core.Address
data class AccessList(val reads: Set<Address> = emptySet(), val writes: Set<Address> = emptySet()) {
fun conflictsWith(other: AccessList): Boolean =
writes.intersect(other.reads + other.writes).isNotEmpty() ||
other.writes.intersect(reads).isNotEmpty()
}

View file

@ -0,0 +1,35 @@
package rip.crit.twist.bips
import java.nio.file.Files
import java.nio.file.Path
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
import rip.crit.twist.proof.ProofOfStore
class FileStorageContract(private val root: Path) : StorageContract {
init {
Files.createDirectories(root)
}
override fun put(content: ByteArray): Hash {
val hash = Sha256.digest(content)
val target = pathFor(hash)
if (Files.notExists(target)) Files.write(target, content)
return hash
}
override fun get(pointer: Hash): ByteArray? {
val path = pathFor(pointer)
if (Files.notExists(path)) return null
val content = Files.readAllBytes(path)
return content.takeIf { Sha256.digest(it) == pointer }
}
fun prove(pointer: Hash, challenge: Hash): ProofOfStore? =
get(pointer)?.let { ProofOfStore.create(pointer, challenge) }
private fun pathFor(hash: Hash): Path {
require(hash.value.matches(Regex("[0-9a-f]{64}"))) { "Invalid content hash" }
return root.resolve(hash.value)
}
}

View file

@ -0,0 +1,23 @@
package rip.crit.twist.bips
import rip.crit.twist.core.Hash
interface StorageContract {
fun put(content: ByteArray): Hash
fun get(pointer: Hash): ByteArray?
}
class FolderStorageContract(private val storage: StorageContract) : StorageContract {
val mounts: MutableMap<String, Hash> = mutableMapOf()
override fun put(content: ByteArray): Hash {
val hash = storage.put(content)
mounts[hash.toString()] = hash
return hash
}
override fun get(pointer: Hash): ByteArray? {
return storage.get(pointer)
}
}

View file

@ -0,0 +1,7 @@
package rip.crit.twist.burn
import rip.crit.twist.core.Amount
fun interface BurnPolicy {
fun amountFor(fee: Amount): Amount
}

View file

@ -0,0 +1,18 @@
package rip.crit.twist.burn
import java.math.BigInteger
import rip.crit.twist.core.Amount
class FractionalBurnPolicy(private val numerator: Long, private val denominator: Long) :
BurnPolicy {
init {
require(numerator in 0..denominator && denominator > 0)
}
override fun amountFor(fee: Amount): Amount =
Amount(
fee.value
.multiply(BigInteger.valueOf(numerator))
.divide(BigInteger.valueOf(denominator))
)
}

View file

@ -0,0 +1,87 @@
package rip.crit.twist.bytecode
import java.nio.ByteBuffer
enum class OpCode(val code: Byte) {
STOP(0),
PUSH32(1),
ADD(2),
SUB(3),
CALLDATA_LOAD(4),
TLOAD(5),
TSTORE(6),
RETURN(7),
MUL(8),
DIV(9),
EQ(10),
LT(11),
GT(12),
AND(13),
OR(14),
NOT(15),
JUMP(16),
JUMPI(17),
DUP(18),
SWAP(19),
LOG(20),
CALL(21),
CREATE(22),
SELFDESTRUCT(23),
REVERT(24),
SLOAD(25),
SSTORE(26),
CALLER(27),
CALLVALUE(28),
TIMESTAMP(29),
BLOCK_NUMBER(30),
BALANCE(31),
SHA256(32),
MOD(33),
XOR(34),
ISZERO(35),
POP(36),
SHL(37),
SHR(38),
BYTE(39),
MLOAD(40),
MSTORE(41),
MSIZE(42),
CALLDATA_SIZE(43),
CODE_SIZE(44),
GAS(45),
ADDRESS(46),
ORIGIN(47),
CHAIN_ID(48),
INVALID(0xFF.toByte()),
}
data class Instruction(val opCode: OpCode, val immediate: ByteArray = byteArrayOf()) {
init {
require(opCode != OpCode.PUSH32 || immediate.size == 32)
}
}
object TwistBytecode {
fun encode(instructions: List<Instruction>): ByteArray =
ByteBuffer.allocate(instructions.sumOf { 1 + it.immediate.size })
.also { output ->
instructions.forEach {
output.put(it.opCode.code)
output.put(it.immediate)
}
}
.array()
fun decode(code: ByteArray): List<Instruction> {
val input = ByteBuffer.wrap(code)
val output = mutableListOf<Instruction>()
while (input.hasRemaining()) {
val code = input.get()
val op = OpCode.entries.firstOrNull { it.code == code } ?: error("Unknown opcode")
val immediate =
if (op == OpCode.PUSH32) ByteArray(32).also(input::get) else byteArrayOf()
output += Instruction(op, immediate)
}
return output
}
}

View file

@ -0,0 +1,100 @@
package rip.crit.twist.compiler
import java.math.BigInteger
import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.bytecode.TwistBytecode
import rip.crit.twist.tvm.Word256
enum class AccessOperation {
READ,
WRITE,
EXECUTE,
}
data class AccessRule(
val operation: AccessOperation,
val resource: String,
val principals: Set<String>,
) {
init {
require(resource.isNotBlank())
require(principals.isNotEmpty())
}
}
data class IrAccessPolicy(val rules: List<AccessRule> = emptyList()) {
fun allows(principal: String, operation: AccessOperation, resource: String): Boolean =
rules.any {
it.operation == operation &&
(it.resource == resource || it.resource == "*") &&
(principal in it.principals || "*" in it.principals)
}
fun require(principal: String, operation: AccessOperation, resource: String) {
require(allows(principal, operation, resource)) {
"$principal lacks ${operation.name.lowercase()} access to $resource"
}
}
}
data class ContractIr(
val name: String,
val functions: List<FunctionIr>,
val accessPolicy: IrAccessPolicy = IrAccessPolicy(),
) {
fun bytecode(): ByteArray = TwistBytecode.encode(functions.flatMap { it.instructions })
}
data class FunctionIr(val name: String, val selector: Int, val instructions: List<Instruction>)
class ContractBuilder(private val name: String) {
private val functions = mutableListOf<FunctionIr>()
private val accessRules = mutableListOf<AccessRule>()
fun allow(operation: AccessOperation, resource: String, vararg principals: String) {
accessRules += AccessRule(operation, resource, principals.toSet())
}
fun function(name: String, selector: Int, body: FunctionBuilder.() -> Unit) {
require(functions.none { it.selector == selector }) { "Duplicate function selector" }
functions += FunctionIr(name, selector, FunctionBuilder().apply(body).build())
}
fun build(): ContractIr = ContractIr(name, functions.toList(), IrAccessPolicy(accessRules))
}
class FunctionBuilder {
private val instructions = mutableListOf<Instruction>()
fun push(value: BigInteger) = emit(OpCode.PUSH32, Word256.of(value).toBytes())
fun push(value: Long) = push(BigInteger.valueOf(value))
fun op(code: OpCode) = emit(code)
fun calldataLoad() = op(OpCode.CALLDATA_LOAD)
fun storageLoad() = op(OpCode.SLOAD)
fun storageStore() = op(OpCode.SSTORE)
fun caller() = op(OpCode.CALLER)
fun callValue() = op(OpCode.CALLVALUE)
fun emitLog() = op(OpCode.LOG)
fun returnWord() = op(OpCode.RETURN)
fun revert() = op(OpCode.REVERT)
internal fun build(): List<Instruction> = instructions.toList()
private fun emit(code: OpCode, immediate: ByteArray = byteArrayOf()) {
instructions += Instruction(code, immediate)
}
}
fun contract(name: String, body: ContractBuilder.() -> Unit): ContractIr =
ContractBuilder(name).apply(body).build()

View file

@ -0,0 +1,166 @@
package rip.crit.twist.compiler
import java.math.BigInteger
import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.tvm.Word256
class LispIrCompiler {
fun parse(source: String): ContractIr {
val forms = Reader(source).readAll()
require(forms.size == 1) { "Expected one contract form" }
val contract = forms.single().list("contract")
require(contract.size >= 2)
val name = contract[1].atom()
val rules = mutableListOf<AccessRule>()
val functions = mutableListOf<FunctionIr>()
contract.drop(2).forEach { form ->
val values = form.list()
when (values.firstOrNull()?.atom()) {
"access" -> rules += parseAccess(values.drop(1))
"function" -> functions += parseFunction(values)
else -> error("Unknown contract form ${values.firstOrNull()}")
}
}
require(functions.map { it.selector }.distinct().size == functions.size) {
"Duplicate function selector"
}
return ContractIr(name, functions, IrAccessPolicy(rules))
}
fun compile(source: String, principal: String): ByteArray {
val ir = parse(source)
ir.functions.forEach { function ->
ir.accessPolicy.require(principal, AccessOperation.EXECUTE, "function:${function.name}")
function.instructions.forEach { instruction ->
when (instruction.opCode) {
OpCode.SLOAD ->
ir.accessPolicy.require(principal, AccessOperation.READ, "storage:*")
OpCode.SSTORE ->
ir.accessPolicy.require(principal, AccessOperation.WRITE, "storage:*")
else -> Unit
}
}
}
return ir.bytecode()
}
private fun parseAccess(forms: List<SExpr>): List<AccessRule> = forms.map { form ->
val values = form.list()
require(values.size >= 3) { "Access rule needs an operation, resource, and principal" }
AccessRule(
AccessOperation.valueOf(values[0].atom().uppercase()),
values[1].atom(),
values.drop(2).map(SExpr::atom).toSet(),
)
}
private fun parseFunction(values: List<SExpr>): FunctionIr {
require(values.size >= 4) { "Function needs a name, selector, and body" }
val instructions = values.drop(3).map(::instruction)
return FunctionIr(values[1].atom(), values[2].atom().toInt(), instructions)
}
private fun instruction(form: SExpr): Instruction {
val values = form.list()
val name = values.first().atom().replace('-', '_').uppercase()
val opcode = OpCode.entries.firstOrNull { it.name == name } ?: error("Unknown IR op $name")
return if (opcode == OpCode.PUSH32) {
require(values.size == 2) { "push32 needs one integer" }
Instruction(opcode, Word256.of(BigInteger(values[1].atom())).toBytes())
} else {
require(values.size == 1) { "$name takes no operands" }
Instruction(opcode)
}
}
}
private sealed interface SExpr {
data class Atom(val value: String) : SExpr
data class Form(val values: List<SExpr>) : SExpr
fun atom(): String = (this as? Atom)?.value ?: error("Expected atom")
fun list(expectedHead: String? = null): List<SExpr> =
(this as? Form)?.values?.also {
if (expectedHead != null)
require(it.firstOrNull()?.atom() == expectedHead) { "Expected $expectedHead form" }
} ?: error("Expected list")
}
private class Reader(source: String) {
private val tokens = tokenize(source)
private var position = 0
fun readAll(): List<SExpr> = buildList { while (position < tokens.size) add(read()) }
private fun read(): SExpr {
require(position < tokens.size) { "Unexpected end of input" }
return when (val token = tokens[position++]) {
"(" -> {
val children = mutableListOf<SExpr>()
while (tokens.getOrNull(position) != ")") children += read()
require(position < tokens.size) { "Unclosed list" }
position++
SExpr.Form(children)
}
")" -> error("Unexpected closing parenthesis")
else -> SExpr.Atom(token)
}
}
private companion object {
fun tokenize(source: String): List<String> {
val output = mutableListOf<String>()
var index = 0
while (index < source.length) {
when {
source[index].isWhitespace() -> index++
source[index] == ';' -> {
while (index < source.length && source[index] != '\n') index++
}
source[index] == '(' || source[index] == ')' ->
output.add(source[index++].toString())
source[index] == '"' -> {
index++
val value = StringBuilder()
while (index < source.length && source[index] != '"') {
if (source[index] == '\\') {
index++
require(index < source.length)
value.append(
when (source[index]) {
'n' -> '\n'
't' -> '\t'
else -> source[index]
}
)
} else value.append(source[index])
index++
}
require(index < source.length) { "Unclosed string" }
index++
output += value.toString()
}
else -> {
val start = index
while (
index < source.length &&
!source[index].isWhitespace() &&
source[index] !in "();"
) index++
output += source.substring(start, index).removePrefix(":")
}
}
}
return output
}
}
}

View file

@ -0,0 +1,43 @@
package rip.crit.twist.compiler
import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.bytecode.TwistBytecode
import rip.crit.twist.jit.LLVMBuilder
import rip.crit.twist.tvm.Word256
object ContractPrelude {
const val SOURCE =
"(pragma twist \"1.0\")\n;; calldata is read with CALLDATA_LOAD and temporary state with TLOAD/TSTORE"
}
class TwistCompiler {
fun compile(source: String): ByteArray =
TwistBytecode.encode(source.lineSequence().mapNotNull(::instruction).toList())
fun lowerToLlvm(source: String, moduleName: String = "contract"): LLVMBuilder =
LLVMBuilder(moduleName, ContractPrelude.SOURCE + "\n" + source)
private fun instruction(line: String): Instruction? {
val tokens = line.trim().split(Regex("\\s+"))
if (tokens.isEmpty() || tokens[0].isBlank() || tokens[0].startsWith(";")) return null
return when (tokens[0].uppercase()) {
"PUSH32" ->
Instruction(
OpCode.PUSH32,
Word256.of(
java.math.BigInteger(
tokens.getOrElse(1) { error("PUSH32 needs a value") }
)
)
.toBytes(),
)
else ->
Instruction(
OpCode.entries.firstOrNull { it.name == tokens[0].uppercase() }
?: error("Unknown instruction ${tokens[0]}")
)
}
}
}

View file

@ -0,0 +1,36 @@
package rip.crit.twist.consensus
import rip.crit.twist.core.Block
import rip.crit.twist.core.Hash
/** Structural consensus gate. Signature and committee policies compose above this layer. */
class BasicConsensusEngine(private val genesisParent: Hash = Hash("genesis")) : ConsensusEngine {
private var running = false
private var tipHeight = -1L
private var tipHash: Hash? = null
override fun start() {
running = true
}
override fun stop() {
running = false
}
override fun validate(block: Block): ValidationResult =
synchronized(this) {
if (!running) return ValidationResult(false, "Consensus engine is stopped")
if (block.header.height < 0 || block.header.timestampMillis < 0)
return ValidationResult(false, "Invalid header")
if (tipHash == null && block.header.parentHash != genesisParent)
return ValidationResult(false, "Invalid genesis parent")
if (
tipHash != null &&
(block.header.parentHash != tipHash || block.header.height != tipHeight + 1)
)
return ValidationResult(false, "Block does not extend canonical tip")
tipHash = block.hash
tipHeight = block.header.height
ValidationResult(true)
}
}

View file

@ -0,0 +1,13 @@
package rip.crit.twist.consensus
import rip.crit.twist.core.Block
interface ConsensusEngine {
fun start()
fun stop()
fun validate(block: Block): ValidationResult
}
data class ValidationResult(val accepted: Boolean, val reason: String? = null)

View file

@ -0,0 +1,10 @@
package rip.crit.twist.core
object HmacSha256 {
fun digest(key: ByteArray, message: ByteArray): ByteArray {
val normalized = (if (key.size > 64) Sha256.bytes(key) else key).copyOf(64)
val inner = ByteArray(64) { (normalized[it].toInt() xor 0x36).toByte() }
val outer = ByteArray(64) { (normalized[it].toInt() xor 0x5c).toByte() }
return Sha256.bytes(outer + Sha256.bytes(inner + message))
}
}

View file

@ -0,0 +1,170 @@
package rip.crit.twist.core
@SmartDoc(
summary = "Pure Kotlin SHA-256 digest API.",
category = "Cryptography",
stability = "stable",
)
object Sha256 {
fun bytes(bytes: ByteArray): ByteArray = Sha256Digest.digest(bytes)
fun digest(bytes: ByteArray): Hash = Hash(bytes(bytes).toHex())
fun digestUtf8(value: String): Hash = digest(value.encodeToByteArray())
fun combine(left: Hash, right: Hash): Hash =
digest((left.value + right.value).encodeToByteArray())
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
}
/** Straightforward FIPS 180-4 SHA-256. Written for clarity, not side-channel resistance. */
object Sha256Digest {
private val initial =
intArrayOf(
0x6a09e667,
0xbb67ae85.toInt(),
0x3c6ef372,
0xa54ff53a.toInt(),
0x510e527f,
0x9b05688c.toInt(),
0x1f83d9ab,
0x5be0cd19,
)
private val constants =
intArrayOf(
0x428a2f98,
0x71374491,
0xb5c0fbcf.toInt(),
0xe9b5dba5.toInt(),
0x3956c25b,
0x59f111f1,
0x923f82a4.toInt(),
0xab1c5ed5.toInt(),
0xd807aa98.toInt(),
0x12835b01,
0x243185be,
0x550c7dc3,
0x72be5d74,
0x80deb1fe.toInt(),
0x9bdc06a7.toInt(),
0xc19bf174.toInt(),
0xe49b69c1.toInt(),
0xefbe4786.toInt(),
0x0fc19dc6,
0x240ca1cc,
0x2de92c6f,
0x4a7484aa,
0x5cb0a9dc,
0x76f988da,
0x983e5152.toInt(),
0xa831c66d.toInt(),
0xb00327c8.toInt(),
0xbf597fc7.toInt(),
0xc6e00bf3.toInt(),
0xd5a79147.toInt(),
0x06ca6351,
0x14292967,
0x27b70a85,
0x2e1b2138,
0x4d2c6dfc,
0x53380d13,
0x650a7354,
0x766a0abb,
0x81c2c92e.toInt(),
0x92722c85.toInt(),
0xa2bfe8a1.toInt(),
0xa81a664b.toInt(),
0xc24b8b70.toInt(),
0xc76c51a3.toInt(),
0xd192e819.toInt(),
0xd6990624.toInt(),
0xf40e3585.toInt(),
0x106aa070,
0x19a4c116,
0x1e376c08,
0x2748774c,
0x34b0bcb5,
0x391c0cb3,
0x4ed8aa4a,
0x5b9cca4f,
0x682e6ff3,
0x748f82ee,
0x78a5636f,
0x84c87814.toInt(),
0x8cc70208.toInt(),
0x90befffa.toInt(),
0xa4506ceb.toInt(),
0xbef9a3f7.toInt(),
0xc67178f2.toInt(),
)
fun digest(message: ByteArray): ByteArray {
val bitLength = message.size.toLong() * 8
val padding = (56 - (message.size + 1) % 64 + 64) % 64
val input = ByteArray(message.size + 1 + padding + 8)
message.copyInto(input)
input[message.size] = 0x80.toByte()
for (i in 0..7) input[input.lastIndex - i] = (bitLength ushr (8 * i)).toByte()
val hash = initial.copyOf()
val words = IntArray(64)
for (offset in input.indices step 64) {
for (i in 0 until 16) {
val p = offset + i * 4
words[i] =
((input[p].toInt() and 0xff) shl 24) or
((input[p + 1].toInt() and 0xff) shl 16) or
((input[p + 2].toInt() and 0xff) shl 8) or
(input[p + 3].toInt() and 0xff)
}
for (i in 16 until 64) {
val s0 =
words[i - 15].rotateRight(7) xor
words[i - 15].rotateRight(18) xor
(words[i - 15] ushr 3)
val s1 =
words[i - 2].rotateRight(17) xor
words[i - 2].rotateRight(19) xor
(words[i - 2] ushr 10)
words[i] = words[i - 16] + s0 + words[i - 7] + s1
}
var a = hash[0]
var b = hash[1]
var c = hash[2]
var d = hash[3]
var e = hash[4]
var f = hash[5]
var g = hash[6]
var h = hash[7]
for (i in 0 until 64) {
val sum1 = e.rotateRight(6) xor e.rotateRight(11) xor e.rotateRight(25)
val choice = (e and f) xor (e.inv() and g)
val temporary1 = h + sum1 + choice + constants[i] + words[i]
val sum0 = a.rotateRight(2) xor a.rotateRight(13) xor a.rotateRight(22)
val majority = (a and b) xor (a and c) xor (b and c)
val temporary2 = sum0 + majority
h = g
g = f
f = e
e = d + temporary1
d = c
c = b
b = a
a = temporary1 + temporary2
}
hash[0] += a
hash[1] += b
hash[2] += c
hash[3] += d
hash[4] += e
hash[5] += f
hash[6] += g
hash[7] += h
}
return ByteArray(32).also { output ->
hash.forEachIndexed { i, value ->
for (j in 0..3) output[i * 4 + j] = (value ushr (24 - j * 8)).toByte()
}
}
}
}

View file

@ -0,0 +1,10 @@
package rip.crit.twist.core
/** Marks a public API for inclusion in generated SmartDoc reference pages. */
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
annotation class SmartDoc(
val summary: String,
val category: String,
val stability: String = "experimental",
)

View file

@ -0,0 +1,56 @@
package rip.crit.twist.core
import java.nio.ByteBuffer
object TransactionHasher {
fun hash(
sender: Address,
recipient: Address?,
nonce: Long,
value: Amount,
payload: ByteArray,
): Hash {
val recipientBytes = recipient?.value?.encodeToByteArray() ?: byteArrayOf()
val senderBytes = sender.value.encodeToByteArray()
val valueBytes = value.value.toByteArray()
val buffer =
ByteBuffer.allocate(
8 +
4 +
senderBytes.size +
4 +
recipientBytes.size +
4 +
valueBytes.size +
4 +
payload.size
)
buffer.putLong(nonce)
buffer.putSized(senderBytes)
buffer.putSized(recipientBytes)
buffer.putSized(valueBytes)
buffer.putSized(payload)
return Sha256.digest(buffer.array())
}
fun create(
sender: Address,
recipient: Address?,
nonce: Long,
value: Amount,
payload: ByteArray = byteArrayOf(),
): Transaction =
Transaction(
hash(sender, recipient, nonce, value, payload),
sender,
recipient,
nonce,
value,
payload.copyOf(),
)
private fun ByteBuffer.putSized(value: ByteArray) {
putInt(value.size)
put(value)
}
}

View file

@ -0,0 +1,6 @@
package rip.crit.twist.core
/** Explicit capability required to enable Twist-only, non-standard protocols. */
sealed class TwistSpecific private constructor() {
data object Enabled : TwistSpecific()
}

View file

@ -0,0 +1,71 @@
package rip.crit.twist.core
import java.math.BigInteger
@JvmInline
value class Address(val value: String) {
init {
require(value.isNotBlank())
}
}
@JvmInline
value class Hash(val value: String) {
init {
require(value.isNotBlank())
}
companion object {
val ZERO = Hash("00000000000000000000000000000000")
}
}
@JvmInline
value class Amount(val value: BigInteger) {
init {
require(value >= BigInteger.ZERO)
}
}
data class Transaction(
val id: Hash,
val sender: Address,
val recipient: Address?,
val nonce: Long,
val value: Amount,
val payload: ByteArray = byteArrayOf(),
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Transaction
if (nonce != other.nonce) return false
if (id != other.id) return false
if (sender != other.sender) return false
if (recipient != other.recipient) return false
if (value != other.value) return false
if (!payload.contentEquals(other.payload)) return false
return true
}
override fun hashCode(): Int {
var result = nonce.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + sender.hashCode()
result = 31 * result + recipient.hashCode()
result = 31 * result + value.hashCode()
result = 31 * result + payload.contentHashCode()
return result
}
}
data class BlockHeader(
val parentHash: Hash,
val stateRoot: Hash,
val height: Long,
val timestampMillis: Long,
)
data class Block(val header: BlockHeader, val transactions: List<Transaction>, val hash: Hash)

View file

@ -0,0 +1,201 @@
package rip.crit.twist.did
import java.math.BigInteger
import java.time.Clock
import java.time.Duration
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
import rip.crit.twist.core.SmartDoc
import rip.crit.twist.core.TwistSpecific
import rip.crit.twist.p2p.DistributedHashTable
import rip.crit.twist.p2p.PeerId
import rip.crit.twist.p2p.PeerRecord
import rip.crit.twist.store.KeyValueStore
@JvmInline
value class Did(val value: String) {
init {
require(value.matches(Regex("did:[a-z0-9]+:[A-Za-z0-9._:%-]+"))) { "Invalid DID" }
}
val method: String
get() = value.substringAfter("did:").substringBefore(':')
val methodSpecificId: String
get() = value.substringAfter("did:$method:")
}
data class VerificationMethod(
val id: String,
val type: String,
val controller: Did,
val publicKeyMultibase: String,
)
data class DidService(val id: String, val type: String, val endpoint: String)
data class DidDocument(
val id: Did,
val verificationMethods: List<VerificationMethod>,
val authentication: List<String> = verificationMethods.map { it.id },
val assertionMethod: List<String> = verificationMethods.map { it.id },
val services: List<DidService> = emptyList(),
) {
fun toJson(): String = buildString {
append("{\"@context\":[\"https://www.w3.org/ns/did/v1.1\"],\"id\":\"")
append(id.value.escape())
append("\",\"verificationMethod\":[")
append(
verificationMethods.joinToString(",") {
"{\"id\":\"${it.id.escape()}\",\"type\":\"${it.type.escape()}\"," +
"\"controller\":\"${it.controller.value.escape()}\"," +
"\"publicKeyMultibase\":\"${it.publicKeyMultibase.escape()}\"}"
}
)
append("],\"authentication\":${strings(authentication)}")
append(",\"assertionMethod\":${strings(assertionMethod)}")
append(
",\"service\":[${services.joinToString(",") { "{\"id\":\"${it.id.escape()}\",\"type\":\"${it.type.escape()}\",\"serviceEndpoint\":\"${it.endpoint.escape()}\"}" }}]"
)
append('}')
}
private fun strings(values: List<String>) =
values.joinToString(",", "[", "]") { "\"${it.escape()}\"" }
private fun String.escape() = replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
}
enum class KeyCodec(val multicodec: Int) {
X25519(0xec),
ED25519(0xed),
SECP256K1(0xe7),
}
@SmartDoc(summary = "Generative did:key identifiers and DID documents.", category = "Identity")
object DidKey {
fun create(publicKey: ByteArray, codec: KeyCodec): Did {
require(publicKey.isNotEmpty())
return Did("did:key:z${Base58.encode(varint(codec.multicodec) + publicKey)}")
}
fun resolve(did: Did): DidDocument {
require(did.method == "key")
val multibase = did.methodSpecificId
require(multibase.startsWith('z')) { "Only base58-btc did:key values are supported" }
val decoded = Base58.decode(multibase.drop(1))
val (codec, prefixSize) = readVarint(decoded)
require(KeyCodec.entries.any { it.multicodec == codec }) { "Unsupported key multicodec" }
require(decoded.size > prefixSize) { "Missing public key" }
val keyId = "${did.value}#$multibase"
return DidDocument(
did,
listOf(VerificationMethod(keyId, "Multikey", did, multibase)),
)
}
private fun varint(value: Int): ByteArray {
var remaining = value
val bytes = mutableListOf<Byte>()
do {
var next = remaining and 0x7f
remaining = remaining ushr 7
if (remaining != 0) next = next or 0x80
bytes += next.toByte()
} while (remaining != 0)
return bytes.toByteArray()
}
private fun readVarint(bytes: ByteArray): Pair<Int, Int> {
var value = 0
var shift = 0
for (index in 0 until minOf(bytes.size, 5)) {
val current = bytes[index].toInt() and 0xff
value = value or ((current and 0x7f) shl shift)
if (current and 0x80 == 0) return value to index + 1
shift += 7
}
error("Invalid multicodec varint")
}
}
/** Project-specific DID method resolved through the Twist peer DHT. */
@SmartDoc(summary = "DHT-backed project-specific did:twist resolver.", category = "Identity")
class DidTwist(
@Suppress("UNUSED_PARAMETER") twistSpecific: TwistSpecific.Enabled,
private val dht: DistributedHashTable,
private val documents: KeyValueStore,
private val clock: Clock = Clock.systemUTC(),
) {
fun create(
peer: PeerId,
publicKey: ByteArray,
endpoints: Set<String>,
ttl: Duration,
codec: KeyCodec = KeyCodec.X25519,
): Did {
require(endpoints.isNotEmpty())
val fingerprint = Sha256.digest(peer.value.encodeToByteArray() + publicKey).value
val did = Did("did:twist:$fingerprint")
dht.put(key(did), PeerRecord(peer, endpoints, clock.instant().plus(ttl)))
documents.put(
did.value.encodeToByteArray(),
byteArrayOf(codec.ordinal.toByte()) + publicKey,
)
return did
}
fun resolve(did: Did): DidDocument? {
require(did.method == "twist")
val record = dht.get(key(did)) ?: return null
val stored = documents.get(did.value.encodeToByteArray()) ?: return null
if (stored.size < 2) return null
val codec = KeyCodec.entries.getOrNull(stored[0].toInt()) ?: return null
val multibase = DidKey.create(stored.copyOfRange(1, stored.size), codec).methodSpecificId
val keyId = "${did.value}#$multibase"
return DidDocument(
did,
listOf(VerificationMethod(keyId, "Multikey", did, multibase)),
listOf(keyId),
listOf(keyId),
record.addresses.sorted().mapIndexed { index, endpoint ->
DidService("${did.value}#overlay-$index", "TwistOverlay", endpoint)
},
)
}
private fun key(did: Did): Hash = Sha256.digestUtf8(did.value)
}
internal object Base58 {
private const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
fun encode(bytes: ByteArray): String {
if (bytes.isEmpty()) return ""
var value = BigInteger(1, bytes)
val output = StringBuilder()
val radix = BigInteger.valueOf(58)
while (value.signum() > 0) {
val parts = value.divideAndRemainder(radix)
output.append(ALPHABET[parts[1].toInt()])
value = parts[0]
}
bytes.takeWhile { it == 0.toByte() }.forEach { _ -> output.append('1') }
return output.reverse().toString()
}
fun decode(value: String): ByteArray {
require(value.isNotEmpty())
var number = BigInteger.ZERO
value.forEach { character ->
val digit = ALPHABET.indexOf(character)
require(digit >= 0) { "Invalid base58-btc character" }
number = number * BigInteger.valueOf(58) + BigInteger.valueOf(digit.toLong())
}
val raw =
number.toByteArray().let {
if (it.size > 1 && it[0] == 0.toByte()) it.drop(1).toByteArray() else it
}
return ByteArray(value.takeWhile { it == '1' }.length) + raw
}
}

View file

@ -0,0 +1,25 @@
package rip.crit.twist.did
import rip.crit.twist.core.TwistSpecific
fun interface DidResolver {
fun resolve(did: Did): DidDocument?
}
class DidMethodRegistry private constructor(private val resolvers: Map<String, DidResolver>) {
fun resolve(did: Did): DidDocument? = resolvers[did.method]?.resolve(did)
companion object {
fun create(
twistSpecific: TwistSpecific? = null,
twist: DidTwist? = null,
): DidMethodRegistry {
require(twist == null || twistSpecific === TwistSpecific.Enabled) {
"did:twist registration requires TwistSpecific.Enabled"
}
val methods = mutableMapOf<String, DidResolver>("key" to DidResolver(DidKey::resolve))
if (twist != null) methods["twist"] = DidResolver(twist::resolve)
return DidMethodRegistry(methods)
}
}
}

View file

@ -0,0 +1,7 @@
package rip.crit.twist.ecdsa
interface Ecdsa {
fun sign(message: ByteArray, privateKey: ByteArray): ByteArray
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean
}

View file

@ -0,0 +1,134 @@
package rip.crit.twist.ecdsa
import java.math.BigInteger
import rip.crit.twist.core.HmacSha256
import rip.crit.twist.core.Sha256
import rip.crit.twist.core.SmartDoc
/**
* secp256k1 ECDSA with RFC 6979 nonces and raw 32-byte private, 65-byte public, and 64-byte
* signature encodings.
*/
@SmartDoc(
summary = "Deterministic secp256k1 ECDSA signing and verification.",
category = "Cryptography",
)
class Secp256k1Ecdsa : Ecdsa {
private data class Point(val x: BigInteger, val y: BigInteger)
private val p =
BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16)
private val n =
BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
private val g =
Point(
BigInteger("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16),
BigInteger("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16),
)
fun publicKey(privateKey: ByteArray): ByteArray {
val d = scalar(privateKey)
val q = multiply(g, d)!!
return byteArrayOf(4) + fixed(q.x) + fixed(q.y)
}
override fun sign(message: ByteArray, privateKey: ByteArray): ByteArray {
val d = scalar(privateKey)
val z = BigInteger(1, Sha256.bytes(message))
var k = nonce(d, z)
while (true) {
val r = multiply(g, k)!!.x.mod(n)
if (r != BigInteger.ZERO) {
var s = k.modInverse(n).multiply(z + r * d).mod(n)
if (s != BigInteger.ZERO) {
if (s > n.shiftRight(1)) s = n - s
return fixed(r) + fixed(s)
}
}
k = k.add(BigInteger.ONE).mod(n)
}
}
override fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean =
runCatching {
require(signature.size == 64)
val r = BigInteger(1, signature.copyOfRange(0, 32))
val s = BigInteger(1, signature.copyOfRange(32, 64))
if (r <= BigInteger.ZERO || r >= n || s <= BigInteger.ZERO || s >= n) return false
val q = decode(publicKey)
val z = BigInteger(1, Sha256.bytes(message))
val w = s.modInverse(n)
val point = add(multiply(g, z * w % n), multiply(q, r * w % n)) ?: return false
point.x.mod(n) == r
}
.getOrDefault(false)
private fun scalar(bytes: ByteArray): BigInteger {
require(bytes.size == 32)
return BigInteger(1, bytes).also { require(it > BigInteger.ZERO && it < n) }
}
private fun decode(bytes: ByteArray): Point {
require(bytes.size == 65 && bytes[0].toInt() == 4)
val q =
Point(BigInteger(1, bytes.copyOfRange(1, 33)), BigInteger(1, bytes.copyOfRange(33, 65)))
require(
q.x.modPow(BigInteger.valueOf(3), p).add(BigInteger.valueOf(7)).mod(p) ==
q.y.modPow(BigInteger.TWO, p)
)
return q
}
private fun add(a: Point?, b: Point?): Point? {
if (a == null) return b
if (b == null) return a
if (a.x == b.x && a.y.add(b.y).mod(p) == BigInteger.ZERO) return null
val slope =
if (a == b)
a.x
.pow(2)
.multiply(BigInteger.valueOf(3))
.multiply(a.y.multiply(BigInteger.TWO).modInverse(p))
else b.y.subtract(a.y).multiply(b.x.subtract(a.x).mod(p).modInverse(p))
val x = slope.pow(2).subtract(a.x).subtract(b.x).mod(p)
return Point(x, slope.multiply(a.x - x).subtract(a.y).mod(p))
}
private fun multiply(point: Point?, scalar: BigInteger): Point? {
var result: Point? = null
var addend = point
for (i in 0 until scalar.bitLength()) {
if (scalar.testBit(i)) result = add(result, addend)
addend = add(addend, addend)
}
return result
}
private fun nonce(d: BigInteger, z: BigInteger): BigInteger {
var v = ByteArray(32) { 1 }
var k = ByteArray(32)
val seed = fixed(d) + fixed(z.mod(n))
k = HmacSha256.digest(k, v + byteArrayOf(0) + seed)
v = HmacSha256.digest(k, v)
k = HmacSha256.digest(k, v + byteArrayOf(1) + seed)
v = HmacSha256.digest(k, v)
while (true) {
v = HmacSha256.digest(k, v)
val candidate = BigInteger(1, v)
if (candidate > BigInteger.ZERO && candidate < n) return candidate
k = HmacSha256.digest(k, v + byteArrayOf(0))
v = HmacSha256.digest(k, v)
}
}
private fun fixed(value: BigInteger): ByteArray {
val bytes = value.toByteArray()
return ByteArray(32).also {
bytes.copyInto(
it,
(32 - bytes.size).coerceAtLeast(0),
(bytes.size - 32).coerceAtLeast(0),
)
}
}
}

View file

@ -0,0 +1,16 @@
package rip.crit.twist.gas
class OutOfGasException(message: String) : IllegalStateException(message)
class BasicGasMeter(override val limit: GasLimit) : GasMeter {
private var consumed = 0L
override val used: GasUsed
get() = GasUsed(consumed)
override fun consume(units: Long) {
require(units >= 0) { "Gas units cannot be negative" }
if (units > limit.value - consumed)
throw OutOfGasException("Gas limit of ${limit.value} exceeded")
consumed += units
}
}

View file

@ -0,0 +1,29 @@
package rip.crit.twist.gas
import java.math.BigInteger
data class GasLimit(val value: Long) {
init {
require(value >= 0)
}
}
data class GasUsed(val value: Long) {
init {
require(value >= 0)
}
}
data class GasPrice(val value: BigInteger) {
init {
require(value >= BigInteger.ZERO)
}
}
interface GasMeter {
val limit: GasLimit
val used: GasUsed
fun consume(units: Long)
}

View file

@ -0,0 +1,13 @@
package rip.crit.twist.gossip
import rip.crit.twist.p2p.NetworkMessage
interface GossipService {
fun start()
fun stop()
fun publish(message: NetworkMessage)
fun subscribe(topic: String, handler: (NetworkMessage) -> Unit)
}

View file

@ -0,0 +1,26 @@
package rip.crit.twist.gossip
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.p2p.NetworkMessage
class InMemoryGossipService : GossipService {
private val subscriptions = ConcurrentHashMap<String, MutableList<(NetworkMessage) -> Unit>>()
private var running = false
override fun start() {
running = true
}
override fun stop() {
running = false
}
override fun publish(message: NetworkMessage) {
check(running)
subscriptions[message.topic]?.toList()?.forEach { it(message) }
}
override fun subscribe(topic: String, handler: (NetworkMessage) -> Unit) {
subscriptions.computeIfAbsent(topic) { mutableListOf() }.add(handler)
}
}

View file

@ -0,0 +1,9 @@
package rip.crit.twist.jit
interface JitCompiler {
fun compile(bytecode: ByteArray): CompiledProgram
}
fun interface CompiledProgram {
fun invoke(input: ByteArray): ByteArray
}

View file

@ -0,0 +1,35 @@
package rip.crit.twist.jit
class LLVMBuilder(val name: String, val module: String) {
fun bitcode(): ByteArray = module.encodeToByteArray()
}
class NativeLlvmRuntime(private val libraryName: String = "twist_llvm") {
private var loaded = false
fun load() {
if (!loaded) {
System.loadLibrary(libraryName)
loaded = true
}
}
fun compile(module: LLVMBuilder, optimizationLevel: Int = 2): NativeArtifact {
require(optimizationLevel in 0..3)
check(loaded) { "Native LLVM runtime is not loaded" }
return NativeArtifact(nativeCompile(module.name, module.bitcode(), optimizationLevel))
}
private external fun nativeCompile(
name: String,
bitcode: ByteArray,
optimizationLevel: Int,
): ByteArray
}
data class NativeArtifact(val machineCode: ByteArray) {
override fun equals(other: Any?): Boolean =
other is NativeArtifact && machineCode.contentEquals(other.machineCode)
override fun hashCode(): Int = machineCode.contentHashCode()
}

View file

@ -0,0 +1,13 @@
package rip.crit.twist.jit
/** Validates input once and binds it to an execution backend for repeat invocation. */
class ValidatedJitCompiler(
private val validate: (ByteArray) -> Unit,
private val execute: (ByteArray, ByteArray) -> ByteArray,
) : JitCompiler {
override fun compile(bytecode: ByteArray): CompiledProgram {
val immutableCode = bytecode.copyOf()
validate(immutableCode)
return CompiledProgram { input -> execute(immutableCode, input.copyOf()).copyOf() }
}
}

View file

@ -0,0 +1,15 @@
package rip.crit.twist.merkle
import rip.crit.twist.core.Hash
interface MerkleTree {
val root: Hash
fun proofFor(leaf: Hash): MerkleProof?
}
data class MerkleProof(
val leaf: Hash,
val siblings: List<Hash>,
val siblingOnLeft: List<Boolean> = emptyList(),
)

View file

@ -0,0 +1,47 @@
package rip.crit.twist.merkle
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
class Sha256MerkleTree(leaves: List<Hash>) : MerkleTree {
private val levels: List<List<Hash>>
override val root: Hash
init {
require(leaves.isNotEmpty()) { "A Merkle tree needs at least one leaf" }
val built = mutableListOf(leaves.toList())
while (built.last().size > 1) {
val level = built.last()
built +=
level.indices.step(2).map { index ->
val left = level[index]
Sha256.combine(left, level.getOrElse(index + 1) { left })
}
}
levels = built
root = levels.last().single()
}
override fun proofFor(leaf: Hash): MerkleProof? {
var index = levels.first().indexOf(leaf)
if (index < 0) return null
val siblings = mutableListOf<Hash>()
val siblingOnLeft = mutableListOf<Boolean>()
for (level in levels.dropLast(1)) {
val siblingIndex =
if (index % 2 == 0) (index + 1).takeIf { it < level.size } ?: index else index - 1
siblings += level[siblingIndex]
siblingOnLeft += index % 2 != 0
index /= 2
}
return MerkleProof(leaf, siblings, siblingOnLeft)
}
}
fun MerkleProof.verifies(expectedRoot: Hash): Boolean {
if (siblings.size != siblingOnLeft.size) return false
return siblings.indices.fold(leaf) { current, index ->
if (siblingOnLeft[index]) Sha256.combine(siblings[index], current)
else Sha256.combine(current, siblings[index])
} == expectedRoot
}

View file

@ -0,0 +1,54 @@
package rip.crit.twist.miner
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import rip.crit.twist.core.Hash
import rip.crit.twist.proof.ProofOfWork
data class MiningResult(val subject: Hash, val difficulty: Int, val proof: ProofOfWork?, val attempts: Long, val elapsedNanos: Long) {
constructor(proof: ProofOfWork?, attempts: Long, elapsedNanos: Long) : this(
proof?.subject ?: Hash.ZERO,
proof?.difficulty ?: 0,
proof,
attempts,
elapsedNanos,
)
}
/** Bounded multi-core miner. Difficulty is the number of leading zero hexadecimal digits. */
class CpuMiner(private val threads: Int = Runtime.getRuntime().availableProcessors()) {
init {
require(threads > 0)
}
fun mine(subject: Hash, difficulty: Int, maxAttempts: Long): MiningResult {
require(difficulty in 0..64)
require(maxAttempts >= 0)
val started = System.nanoTime()
val found = AtomicBoolean(false)
val attempts = AtomicLong(0)
val executor = Executors.newFixedThreadPool(threads)
return try {
val tasks =
(0 until threads).map { lane ->
Callable {
var nonce = lane.toLong()
while (nonce < maxAttempts && !found.get()) {
attempts.incrementAndGet()
val proof = ProofOfWork(subject, nonce, difficulty)
if (proof.verify() && found.compareAndSet(false, true))
return@Callable proof
nonce += threads
}
null
}
}
val proof = executor.invokeAll(tasks).firstNotNullOfOrNull { it.get() }
MiningResult(subject, difficulty, proof, attempts.get(), System.nanoTime() - started)
} finally {
executor.shutdownNow()
}
}
}

View file

@ -0,0 +1,7 @@
package rip.crit.twist.mint
import rip.crit.twist.core.Amount
fun interface MintPolicy {
fun rewardAt(height: Long): Amount
}

View file

@ -0,0 +1,17 @@
package rip.crit.twist.mint
import rip.crit.twist.core.Amount
class FixedRewardPolicy(private val reward: Amount) : MintPolicy {
override fun rewardAt(height: Long): Amount {
require(height >= 0)
return reward
}
}
class HalvingRewardPolicy(private val initial: Amount, private val interval: Long) : MintPolicy {
override fun rewardAt(height: Long): Amount {
require(height >= 0 && interval > 0)
return Amount(initial.value.shiftRight((height / interval).toInt()))
}
}

View file

@ -0,0 +1,53 @@
package rip.crit.twist.ope
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import rip.crit.twist.access.AccessList
import rip.crit.twist.core.Transaction
import rip.crit.twist.tvm.ExecutionResult
/** Executes conflict-free batches concurrently while preserving input result order. */
class AccessParallelExecutor(
private val access: (Transaction) -> AccessList,
private val executeOne: (Transaction) -> ExecutionResult,
private val threads: Int = Runtime.getRuntime().availableProcessors(),
) : ParallelExecutor {
init {
require(threads > 0)
}
override fun execute(transactions: List<Transaction>): List<ExecutionResult> {
val indexed = transactions.mapIndexed { index, transaction ->
IndexedValue(index, transaction)
}
val pending = ArrayDeque(indexed)
val results = arrayOfNulls<ExecutionResult>(transactions.size)
val pool = Executors.newFixedThreadPool(threads)
try {
while (pending.isNotEmpty()) {
val batch = mutableListOf<IndexedValue<Transaction>>()
val deferred = ArrayDeque<IndexedValue<Transaction>>()
while (pending.isNotEmpty()) {
val candidate = pending.removeFirst()
if (batch.none { access(it.value).conflictsWith(access(candidate.value)) }) {
batch += candidate
} else {
deferred += candidate
}
}
pending.addAll(deferred)
val completed =
pool.invokeAll(
batch.map { item -> Callable { item.index to executeOne(item.value) } }
)
completed.forEach { future ->
val (index, result) = future.get()
results[index] = result
}
}
} finally {
pool.shutdownNow()
}
return results.map { requireNotNull(it) }
}
}

View file

@ -0,0 +1,8 @@
package rip.crit.twist.ope
import rip.crit.twist.core.Transaction
import rip.crit.twist.tvm.ExecutionResult
interface ParallelExecutor {
fun execute(transactions: List<Transaction>): List<ExecutionResult>
}

View file

@ -0,0 +1,14 @@
package rip.crit.twist.ope
import rip.crit.twist.core.Transaction
import rip.crit.twist.state.WorldState
import rip.crit.twist.tvm.ExecutionResult
import rip.crit.twist.tvm.VirtualMachine
class SequentialExecutor(private val vm: VirtualMachine, private val state: WorldState) :
ParallelExecutor {
override fun execute(transactions: List<Transaction>): List<ExecutionResult> =
transactions.map {
vm.execute(it, state)
}
}

View file

@ -0,0 +1,243 @@
package rip.crit.twist.p2p
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import rip.crit.twist.core.Hash
import rip.crit.twist.core.SmartDoc
enum class NetworkAdapterKind {
DEVP2P,
LIBP2P,
TWIST_OVERLAY,
}
interface NetworkAdapter {
val kind: NetworkAdapterKind
fun encode(message: NetworkMessage): ByteArray
fun decode(frame: ByteArray): NetworkMessage
}
open class EnvelopeAdapter(override val kind: NetworkAdapterKind) : NetworkAdapter {
override fun encode(message: NetworkMessage): ByteArray =
listOf(
message.id.value,
message.topic,
message.payload.joinToString("") { "%02x".format(it) },
)
.joinToString("\n")
.encodeToByteArray()
override fun decode(frame: ByteArray): NetworkMessage {
val fields = frame.decodeToString().split("\n", limit = 3)
require(fields.size == 3)
val payload = fields[2].chunked(2).map { it.toInt(16).toByte() }.toByteArray()
return NetworkMessage(fields[1], payload, Hash(fields[0]))
}
}
data class ProtocolCapability(val name: String, val version: Int) {
init {
require(name.matches(Regex("[a-z0-9./-]{1,64}")))
require(version >= 0)
}
}
data class DevP2pHello(
val clientId: String,
val listenPort: Int,
val nodeId: PeerId,
val capabilities: Set<ProtocolCapability>,
)
data class Libp2pIdentify(
val peer: PeerId,
val protocols: Set<String>,
val addresses: Set<String>,
)
/** Twist-native capability negotiation with devp2p Hello RLP semantics. */
@SmartDoc(
summary = "devp2p Hello/Ping/Pong/Disconnect RLP wire with capability negotiation.",
category = "Networking",
)
class DevP2pAdapter(
private val hello: DevP2pHello,
private val capabilityOffset: Int = 16,
) : NetworkAdapter {
constructor(
capabilities: Set<ProtocolCapability> = setOf(ProtocolCapability("twist", 1))
) : this(
DevP2pHello("twist/1.0.0", 0, PeerId("bootstrap"), capabilities),
)
override val kind: NetworkAdapterKind = NetworkAdapterKind.DEVP2P
fun helloFrame(): ByteArray =
byteArrayOf(Rlpx.MSG_HELLO.toByte()) + Rlpx.encodeHello(hello)
fun pingFrame(): ByteArray = byteArrayOf(Rlpx.MSG_PING.toByte()) + Rlpx.encodePing()
fun pongFrame(): ByteArray = byteArrayOf(Rlpx.MSG_PONG.toByte()) + Rlpx.encodePong()
fun disconnectFrame(reason: Int = Rlpx.DISC_REQUESTED): ByteArray =
byteArrayOf(Rlpx.MSG_DISCONNECT.toByte()) + Rlpx.encodeDisconnect(reason)
override fun encode(message: NetworkMessage): ByteArray =
Rlpx.encodeSubprotocolMessage(capabilityOffset, 0, WireEnvelope.encode(message))
override fun decode(frame: ByteArray): NetworkMessage {
val (subId, payload) = Rlpx.decodeSubprotocolMessage(frame, capabilityOffset)
require(subId == 0) { "Unknown devp2p-twist subprotocol id" }
return WireEnvelope.decode(payload)
}
fun decodeBase(frame: ByteArray): BaseMessage =
when ((frame[0].toInt() and 0xFF)) {
Rlpx.MSG_HELLO -> BaseMessage.Hello(Rlpx.decodeHello(frame.copyOfRange(1, frame.size)))
Rlpx.MSG_DISCONNECT ->
BaseMessage.Disconnect(Rlpx.decodeDisconnect(frame.copyOfRange(1, frame.size)))
Rlpx.MSG_PING -> BaseMessage.Ping
Rlpx.MSG_PONG -> BaseMessage.Pong
else -> error("Unknown devp2p base message")
}
fun negotiate(remote: DevP2pHello): Set<ProtocolCapability> =
hello.capabilities
.groupBy { it.name }
.mapNotNull { (name, local) ->
remote.capabilities
.filter { it.name == name }
.map { it.version }
.intersect(local.map { it.version }.toSet())
.maxOrNull()
?.let { ProtocolCapability(name, it) }
}
.toSet()
sealed interface BaseMessage {
data class Hello(val hello: DevP2pHello) : BaseMessage
data class Disconnect(val reason: Int) : BaseMessage
data object Ping : BaseMessage
data object Pong : BaseMessage
}
}
/** libp2p multistream-select + Identify + Gossipsub-publish stream framing. */
@SmartDoc(
summary = "libp2p multistream-select, Identify protobuf, and Gossipsub publish envelopes.",
category = "Networking",
)
class LibP2pAdapter(
private val protocols: Set<String> = setOf(Libp2p.GOSSIPSUB_PROTO),
private val agent: String = "twist/1.0.0",
) : NetworkAdapter {
override val kind: NetworkAdapterKind = NetworkAdapterKind.LIBP2P
init {
require(protocols.all { it.startsWith('/') && it.length <= 128 })
}
fun multistreamHandshake(protocol: String = Libp2p.GOSSIPSUB_PROTO): ByteArray =
Libp2p.frameLsForTest(Libp2p.MULTISTREAM) +
Libp2p.multistreamPropose(protocol)
override fun encode(message: NetworkMessage): ByteArray {
val publish =
Libp2p.encodeGossipPublish(
message.id.value.encodeToByteArray(), message.payload, 0L, message.topic)
val header = Libp2p.yamuxHeader(Libp2p.YAMUX_DATA, 0, 1, publish.size)
return header + publish
}
override fun decode(frame: ByteArray): NetworkMessage {
require(frame.size >= 12)
val header = Libp2p.yamuxParse(frame.copyOfRange(0, 12))
require(header.type == Libp2p.YAMUX_DATA)
val publish =
Libp2p.decodeGossipPublish(
frame.copyOfRange(12, 12 + header.length.coerceAtMost(frame.size - 12)))
return NetworkMessage(publish.topic, publish.data, Hash(publish.from.decodeToString()))
}
fun select(remoteProtocols: Set<String>): String? =
protocols.sorted().firstOrNull { it in remoteProtocols }
fun identify(peer: PeerId, addresses: Set<String>): Libp2pIdentify =
Libp2pIdentify(peer, protocols, addresses)
fun identifyFrame(peer: PeerId, addresses: Set<String>, observed: String = ""): ByteArray =
Libp2p.encodeIdentify(agent, protocols, observed, addresses)
}
class LocalPeerNetwork : PeerNetwork {
private val connected = linkedSetOf<PeerId>()
private var running = false
val received = mutableListOf<NetworkMessage>()
override fun start() {
running = true
}
override fun stop() {
running = false
}
override fun peers(): Set<PeerId> = connected.toSet()
override fun broadcast(message: NetworkMessage) {
check(running)
received += message
}
fun connect(peer: PeerId) {
connected += peer
}
}
object WireEnvelope {
private const val VERSION: Byte = 1
private const val MAX_FIELD_SIZE = 16 * 1024 * 1024
fun encode(message: NetworkMessage): ByteArray {
val output = ByteArrayOutputStream()
DataOutputStream(output).use { data ->
data.writeByte(VERSION.toInt())
val idBytes = message.id.value.encodeToByteArray()
require(idBytes.size <= MAX_FIELD_SIZE) { "Invalid id size" }
data.writeInt(idBytes.size)
data.write(idBytes)
val topicBytes = message.topic.encodeToByteArray()
require(topicBytes.size <= MAX_FIELD_SIZE) { "Invalid topic size" }
data.writeInt(topicBytes.size)
data.write(topicBytes)
require(message.payload.size <= MAX_FIELD_SIZE) { "Invalid payload size" }
data.writeInt(message.payload.size)
data.write(message.payload)
}
return output.toByteArray()
}
fun decode(bytes: ByteArray): NetworkMessage =
DataInputStream(ByteArrayInputStream(bytes)).use { input ->
require(input.readByte() == VERSION) { "Unsupported wire version" }
val idSize = input.readInt()
require(idSize in 0..MAX_FIELD_SIZE) { "Invalid id size" }
val idBytes = ByteArray(idSize).also(input::readFully)
val topicSize = input.readInt()
require(topicSize in 0..MAX_FIELD_SIZE) { "Invalid topic size" }
val topicBytes = ByteArray(topicSize).also(input::readFully)
val payloadSize = input.readInt()
require(payloadSize in 0..MAX_FIELD_SIZE) { "Invalid payload size" }
val payload = ByteArray(payloadSize).also(input::readFully)
require(input.available() == 0) { "Trailing message bytes" }
NetworkMessage(topicBytes.decodeToString(), payload, Hash(idBytes.decodeToString()))
}
}

View file

@ -0,0 +1,47 @@
package rip.crit.twist.p2p
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.core.Hash
data class PeerRecord(val peer: PeerId, val addresses: Set<String>, val expiresAt: Instant)
interface DistributedHashTable {
fun put(key: Hash, record: PeerRecord)
fun get(key: Hash): PeerRecord?
fun closest(key: Hash, limit: Int = 20): List<PeerRecord>
}
class InMemoryDht(private val clock: Clock = Clock.systemUTC()) : DistributedHashTable {
private val records = ConcurrentHashMap<Hash, PeerRecord>()
override fun put(key: Hash, record: PeerRecord) {
require(record.expiresAt > clock.instant())
records[key] = record
}
override fun get(key: Hash): PeerRecord? =
records[key]?.takeIf { it.expiresAt > clock.instant() }
override fun closest(key: Hash, limit: Int): List<PeerRecord> {
require(limit > 0)
val target = key.value.toBigInteger(16)
return records.entries
.mapNotNull { (hash, record) ->
get(hash)?.let { (hash.value.toBigInteger(16).xor(target)) to it }
}
.sortedBy { it.first }
.take(limit)
.map { it.second }
}
fun announce(peer: PeerId, addresses: Set<String>, ttl: Duration = Duration.ofHours(1)): Hash {
val key = Hash(peer.value.padEnd(64, '0').take(64))
put(key, PeerRecord(peer, addresses, clock.instant().plus(ttl)))
return key
}
}

View file

@ -0,0 +1,62 @@
package rip.crit.twist.p2p
import java.nio.file.Files
import java.nio.file.Path
import java.time.Clock
import java.time.Instant
import java.util.Base64
import rip.crit.twist.core.Hash
class FileDht(private val root: Path, private val clock: Clock = Clock.systemUTC()) :
DistributedHashTable {
init {
Files.createDirectories(root)
}
override fun put(key: Hash, record: PeerRecord) {
require(record.expiresAt > clock.instant())
Files.write(
root.resolve(key.value),
listOf(
record.peer.value,
record.expiresAt.toEpochMilli().toString(),
record.addresses.joinToString("\t") {
Base64.getUrlEncoder().withoutPadding().encodeToString(it.encodeToByteArray())
},
),
)
}
override fun get(key: Hash): PeerRecord? = runCatching {
val lines = Files.readAllLines(root.resolve(key.value))
val record =
PeerRecord(
PeerId(lines[0]),
lines[2]
.split("\t")
.filter(String::isNotEmpty)
.map { Base64.getUrlDecoder().decode(it).decodeToString() }
.toSet(),
Instant.ofEpochMilli(lines[1].toLong()),
)
record.takeIf { it.expiresAt > clock.instant() }
}
.getOrNull()
override fun closest(key: Hash, limit: Int): List<PeerRecord> {
require(limit > 0)
val target = key.value.toBigInteger(16)
return Files.list(root).use { paths ->
paths
.map { it.fileName.toString() }
.filter { it.matches(Regex("[0-9a-fA-F]+")) }
.map { Hash(it) }
.map { hash -> hash.value.toBigInteger(16).xor(target) to get(hash) }
.filter { it.second != null }
.sorted(compareBy { it.first })
.limit(limit.toLong())
.map { it.second!! }
.toList()
}
}
}

View file

@ -0,0 +1,224 @@
package rip.crit.twist.p2p
import kotlin.random.Random
import rip.crit.twist.core.HmacSha256
import rip.crit.twist.core.SmartDoc
/**
* libp2p transport shape (specs: multistream-select, Noise XX handshake, Yamux/mplex
* framing, Identify protobuf, Gossipsub v1.1 RPC).
*
* Multistream-select: initiator sends `/multistream/1.0.0\n`, then for each proposal
* `varint-len || "<proto>\n"`; responder answers `varint-len || "<proto>\n"` on accept
* or `varint-len || "na\n"` on reject.
*
* Noise XX (`[e, ->e/NoPayload, e...ee|s...es|s...se]`): three messages carrying
* ephemeral pubkeys, static pubkeys (encrypted), and payloads, chained with
* `MixHash/HKDF` into transport keys sealing subsequent frames with AES-GCM.
*
* Yamux: 12-byte header `version(1) || type || flags(2BE) || stream-id(4BE) || length(4BE)`
* with types Data(0), WindowUpdate(1), Ping(2), GoAway(3).
*
* Identify: protobuf with canonical field numbers (1 agent, 2 protocols, 3 observed-addr,
* 4 listen-addrs, 5 protocols-versions... simplified here to 1..4 + 6 public-key + 8-signed-peer-record
* envelope); Gossipsub RPC: `Publish(topic, seqno, from, data, signature)` with v1.1
* mesh parameters (`D=6, Dlo=4, Dhi=12, heartbeat=1s`).
*
* Twist profile: Noise DH/static payloads use bundled X25519 + HKDF-SHA256 + AES-GCM;
* protobuf fields use length-delimited varint encoding with the canonical field numbers above.
*/
object Libp2p {
const val MULTISTREAM = "/multistream/1.0.0"
const val NOISE_PROTO = "/noise"
const val YAMUX_PROTO = "/yamux/1.0.0"
const val MPLEX_PROTO = "/mplex/6.7.0"
const val IDENTIFY_PROTO = "/ipfs/id/1.0.0"
const val GOSSIPSUB_PROTO = "/meshsub/1.1.0"
const val YAMUX_DATA = 0
const val YAMUX_WINDOW = 1
const val YAMUX_PING = 2
const val YAMUX_GOAWAY = 3
const val GOSSIP_D = 6
const val GOSSIP_D_LO = 4
const val GOSSIP_D_HI = 12
const val GOSSIP_HEARTBEAT_MS = 1000L
fun frameLsForTest(line: String): ByteArray = frameLs("$line\n".encodeToByteArray())
fun multistreamPropose(protocol: String): ByteArray =
frameLs("$protocol\n".encodeToByteArray())
fun multistreamAccept(protocol: String): ByteArray =
frameLs("$protocol\n".encodeToByteArray())
fun multistreamReject(): ByteArray = frameLs("na\n".encodeToByteArray())
fun multistreamRead(frame: ByteArray): String {
val body = unframeLs(frame).decodeToString()
require(body.endsWith("\n")) { "Bad multistream line" }
return body.dropLast(1)
}
fun yamuxHeader(type: Int, flags: Int, streamId: Int, length: Int): ByteArray {
require(type in 0..3 && flags in 0..0xFFFF && length >= 0)
return byteArrayOf(
0,
type.toByte(),
((flags ushr 8) and 0xFF).toByte(),
(flags and 0xFF).toByte(),
((streamId ushr 24) and 0xFF).toByte(),
((streamId ushr 16) and 0xFF).toByte(),
((streamId ushr 8) and 0xFF).toByte(),
(streamId and 0xFF).toByte(),
((length ushr 24) and 0xFF).toByte(),
((length ushr 16) and 0xFF).toByte(),
((length ushr 8) and 0xFF).toByte(),
(length and 0xFF).toByte())
}
fun yamuxParse(header: ByteArray): YamuxHeader {
require(header.size == 12 && header[0].toInt() == 0) { "Bad Yamux header" }
val type = header[1].toInt() and 0xFF
val flags = ((header[2].toInt() and 0xFF) shl 8) or (header[3].toInt() and 0xFF)
var streamId = 0
var length = 0
for (i in 4..7) streamId = (streamId shl 8) or (header[i].toInt() and 0xFF)
for (i in 8..11) length = (length shl 8) or (header[i].toInt() and 0xFF)
require(type in 0..3 && length >= 0)
return YamuxHeader(type, flags, streamId, length)
}
data class YamuxHeader(val type: Int, val flags: Int, val streamId: Int, val length: Int)
// ---- minimal protobuf (varint + length-delimited) with canonical Identify fields ----
fun varint(value: Long): ByteArray {
require(value >= 0)
var v = value
val out = mutableListOf<Byte>()
while (true) {
var b = (v and 0x7F).toInt()
v = v ushr 7
if (v != 0L) b = b or 0x80
out += b.toByte()
if (v == 0L) break
}
return out.toByteArray()
}
fun varintRead(bytes: ByteArray, offset: Int): Pair<Long, Int> {
var result = 0L
var shift = 0
var pos = offset
while (pos < bytes.size) {
val b = bytes[pos++].toInt() and 0xFF
result = result or ((b and 0x7F).toLong() shl shift)
shift += 7
require(shift <= 64) { "Varint overflow" }
if (b and 0x80 == 0) return Pair(result, pos)
}
error("Truncated varint")
}
fun field(number: Int, payload: ByteArray): ByteArray =
varint(((number shl 3) or 2).toLong()) + varint(payload.size.toLong()) + payload
fun fieldString(number: Int, value: String): ByteArray =
field(number, value.encodeToByteArray())
fun parseFields(bytes: ByteArray): Map<Int, List<ByteArray>> {
val out = mutableMapOf<Int, MutableList<ByteArray>>()
var pos = 0
while (pos < bytes.size) {
val (key, afterKey) = varintRead(bytes, pos)
pos = afterKey
require((key and 7) == 2L) { "Only length-delimited fields supported" }
val number = (key ushr 3).toInt()
val (len, afterLen) = varintRead(bytes, pos)
pos = afterLen
require(len in 0..16 * 1024 * 1024 && pos + len <= bytes.size) { "Bad protobuf length" }
out.getOrPut(number) { mutableListOf() } += bytes.copyOfRange(pos, pos + len.toInt())
pos += len.toInt()
}
return out
}
/** Identify fields: 1 agent, 2 protocols (repeated), 3 observed-addr, 4 listen-addrs (repeated). */
fun encodeIdentify(agent: String, protocols: Set<String>, observed: String, listen: Set<String>): ByteArray {
var out = fieldString(1, agent)
protocols.sorted().forEach { out += fieldString(2, it) }
out += fieldString(3, observed)
listen.sorted().forEach { out += fieldString(4, it) }
return out
}
fun decodeIdentify(bytes: ByteArray): Libp2pIdentifyFull {
val fields = parseFields(bytes)
val agent = fields[1]?.firstOrNull()?.decodeToString() ?: ""
val protocols = fields[2].orEmpty().map { it.decodeToString() }.toSet()
val observed = fields[3]?.firstOrNull()?.decodeToString() ?: ""
val listen = fields[4].orEmpty().map { it.decodeToString() }.toSet()
return Libp2pIdentifyFull(agent, protocols, observed, listen)
}
data class Libp2pIdentifyFull(
val agent: String,
val protocols: Set<String>,
val observedAddr: String,
val listenAddrs: Set<String>,
)
/** Gossipsub Publish fields: 1 from, 2 data, 3 seqno, 4 topic, 5 signature, 6 key. */
fun encodeGossipPublish(
from: ByteArray,
data: ByteArray,
seqno: Long,
topic: String,
signature: ByteArray = ByteArray(0),
): ByteArray {
var out = field(1, from)
out += field(2, data)
out += field(3, seqnoBytes(seqno))
out += fieldString(4, topic)
if (signature.isNotEmpty()) out += field(5, signature)
return out
}
fun decodeGossipPublish(bytes: ByteArray): GossipPublish {
val fields = parseFields(bytes)
val from = fields[1]?.firstOrNull() ?: error("Gossip publish missing from")
val data = fields[2]?.firstOrNull() ?: error("Gossip publish missing data")
val seqnoRaw = fields[3]?.firstOrNull() ?: error("Gossip publish missing seqno")
require(seqnoRaw.size == 8)
var seqno = 0L
for (b in seqnoRaw) seqno = (seqno shl 8) or (b.toLong() and 0xFF)
val topic = fields[4]?.firstOrNull()?.decodeToString() ?: ""
return GossipPublish(from, data, seqno, topic, fields[5]?.firstOrNull() ?: ByteArray(0))
}
data class GossipPublish(
val from: ByteArray,
val data: ByteArray,
val seqno: Long,
val topic: String,
val signature: ByteArray,
)
private fun seqnoBytes(seqno: Long): ByteArray {
val out = ByteArray(8)
for (i in 0..7) out[7 - i] = ((seqno ushr (8 * i)) and 0xFF).toByte()
return out
}
private fun frameLs(body: ByteArray): ByteArray = varint(body.size.toLong()) + body
private fun unframeLs(frame: ByteArray): ByteArray {
val (len, pos) = varintRead(frame, 0)
require(len in 0..1024 && pos + len == frame.size.toLong()) { "Bad multistream frame" }
return frame.copyOfRange(pos, frame.size)
}
}
// NoiseXxSession / NoiseTransport: see transport/NoiseCrypto.kt.

View file

@ -0,0 +1,37 @@
package rip.crit.twist.p2p
import rip.crit.twist.core.Hash
@JvmInline value class PeerId(val value: String)
data class NetworkMessage(val topic: String, val payload: ByteArray, val id: Hash) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as NetworkMessage
if (topic != other.topic) return false
if (!payload.contentEquals(other.payload)) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = topic.hashCode()
result = 31 * result + payload.contentHashCode()
result = 31 * result + id.hashCode()
return result
}
}
interface PeerNetwork {
fun start()
fun stop()
fun peers(): Set<PeerId>
fun broadcast(message: NetworkMessage)
}

View file

@ -0,0 +1,36 @@
package rip.crit.twist.p2p
import rip.crit.twist.core.TwistSpecific
data class ProtocolDescriptor(
val name: String,
val version: String,
val transports: Set<String>,
val encrypted: Boolean,
val interoperable: Boolean,
)
object ProtocolCatalog {
val devP2p =
ProtocolDescriptor(
"devp2p-rlpx",
"rlpx-eip8-v4/twist-profile-v1",
setOf("tcp"),
true,
true,
)
val libP2p =
ProtocolDescriptor(
"libp2p-noise-yamux",
"noise-xx/yamux/meshsub-1.1.0",
setOf("tcp"),
true,
true,
)
private val twistOverlay =
ProtocolDescriptor("twist-overlay", "1.0", setOf("tcp", "memory"), true, true)
fun advertised(twistSpecific: TwistSpecific? = null): List<ProtocolDescriptor> =
listOf(devP2p, libP2p) +
if (twistSpecific === TwistSpecific.Enabled) listOf(twistOverlay) else emptyList()
}

View file

@ -0,0 +1,145 @@
package rip.crit.twist.p2p
import java.io.ByteArrayOutputStream
import java.math.BigInteger
/**
* Ethereum Recursive Length Prefix (RLP) codec.
*
* Wire reference: ethereum/devp2p `rlpx.md` — RLPx framing, auth/ack handshake
* payloads, and devp2p Hello/Disconnect/Ping/Pong bodies are all RLP-encoded.
* Single-byte values < 0x80 are their own encoding; all other strings/lists use
* short (<=55 bytes) and long length prefixes.
*/
object PRlp {
private const val SHORT_MAX = 55
fun encodeString(bytes: ByteArray): ByteArray =
when {
bytes.size == 1 && (bytes[0].toInt() and 0xFF) < 0x80 -> bytes.copyOf()
bytes.size <= SHORT_MAX ->
byteArrayOf((0x80 + bytes.size).toByte()) + bytes
else -> {
val len = lengthBytes(bytes.size)
byteArrayOf((0xB7 + len.size).toByte()) + len + bytes
}
}
fun encodeInt(value: BigInteger): ByteArray {
require(value >= BigInteger.ZERO) { "RLP integers must be non-negative" }
if (value == BigInteger.ZERO) return byteArrayOf(0x80.toByte())
var raw = value.toByteArray().dropWhile { it == 0.toByte() }.toByteArray()
return encodeString(raw)
}
fun encodeInt(value: Long): ByteArray = encodeInt(BigInteger.valueOf(value))
fun encodeList(items: List<ByteArray>): ByteArray {
val payload = items.fold(ByteArray(0)) { acc, item -> acc + item }
return when {
payload.size <= SHORT_MAX ->
byteArrayOf((0xC0 + payload.size).toByte()) + payload
else -> {
val len = lengthBytes(payload.size)
byteArrayOf((0xF7 + len.size).toByte()) + len + payload
}
}
}
fun encodeElements(vararg items: ByteArray): ByteArray = encodeList(items.toList())
/** Decodes one RLP item starting at [offset]; returns (item, nextOffset). */
fun decodeOne(bytes: ByteArray, offset: Int = 0): Pair<PRlpItem, Int> {
require(offset < bytes.size) { "RLP truncated" }
val prefix = bytes[offset].toInt() and 0xFF
return when {
prefix < 0x80 -> Pair(PRlpItem.String(bytes.copyOfRange(offset, offset + 1)), offset + 1)
prefix <= 0xB7 -> {
val len = prefix - 0x80
require(offset + 1 + len <= bytes.size) { "RLP string truncated" }
Pair(PRlpItem.String(bytes.copyOfRange(offset + 1, offset + 1 + len)), offset + 1 + len)
}
prefix <= 0xBF -> {
val lenOfLen = prefix - 0xB7
val len = readLength(bytes, offset + 1, lenOfLen)
require(offset + 1 + lenOfLen + len <= bytes.size) { "RLP long string truncated" }
Pair(
PRlpItem.String(bytes.copyOfRange(offset + 1 + lenOfLen, offset + 1 + lenOfLen + len)),
offset + 1 + lenOfLen + len)
}
prefix <= 0xF7 -> {
val len = prefix - 0xC0
require(offset + 1 + len <= bytes.size) { "RLP list truncated" }
Pair(decodeList(bytes.copyOfRange(offset + 1, offset + 1 + len)), offset + 1 + len)
}
else -> {
val lenOfLen = prefix - 0xF7
val len = readLength(bytes, offset + 1, lenOfLen)
require(offset + 1 + lenOfLen + len <= bytes.size) { "RLP long list truncated" }
Pair(
decodeList(bytes.copyOfRange(offset + 1 + lenOfLen, offset + 1 + lenOfLen + len)),
offset + 1 + lenOfLen + len)
}
}
}
fun decode(bytes: ByteArray): PRlpItem {
val (item, next) = decodeOne(bytes, 0)
require(next == bytes.size) { "Trailing RLP bytes" }
return item
}
private fun decodeList(payload: ByteArray): PRlpItem.List {
val items = mutableListOf<PRlpItem>()
var offset = 0
while (offset < payload.size) {
val (item, next) = decodeOne(payload, offset)
items += item
offset = next
}
require(offset == payload.size) { "RLP list overrun" }
return PRlpItem.List(items)
}
private fun readLength(bytes: ByteArray, offset: Int, lenOfLen: Int): Int {
require(lenOfLen in 1..4 && offset + lenOfLen <= bytes.size) { "Bad RLP length prefix" }
require(bytes[offset] != 0.toByte()) { "RLP length has leading zero" }
var len = 0
for (i in 0 until lenOfLen) len = (len shl 8) or (bytes[offset + i].toInt() and 0xFF)
require(len > SHORT_MAX) { "RLP long form used for short payload" }
return len
}
private fun lengthBytes(size: Int): ByteArray {
val out = ByteArrayOutputStream()
var v = size
val tmp = mutableListOf<Byte>()
while (v > 0) {
tmp += (v and 0xFF).toByte()
v = v ushr 8
}
tmp.reversed().forEach { out.write(it.toInt()) }
return out.toByteArray()
}
}
sealed interface PRlpItem {
data class String(val bytes: ByteArray) : PRlpItem {
fun asLong(): Long {
require(bytes.isNotEmpty()) { "Empty RLP int" }
require(!(bytes.size > 1 && bytes[0] == 0.toByte())) { "Non-canonical RLP int" }
var v = 0L
for (b in bytes) v = (v shl 8) or (b.toLong() and 0xFF)
return v
}
fun asText(): kotlin.String = bytes.decodeToString()
override fun equals(other: Any?): Boolean =
other is String && bytes.contentEquals(other.bytes)
override fun hashCode(): Int = bytes.contentHashCode()
}
data class List(val items: kotlin.collections.List<PRlpItem>) : PRlpItem
}

View file

@ -0,0 +1,187 @@
package rip.crit.twist.p2p
import kotlin.random.Random
import rip.crit.twist.core.HmacSha256
import rip.crit.twist.core.SmartDoc
/**
* RLPx transport shape (ethereum/devp2p `rlpx.md`, EIP-8, wire `p2p.md`).
*
* Handshake: initiator -> auth (`uint16BE size || box || pad`), recipient -> ack (same
* framing). Plaintexts are RLP: `auth = [sig, initiator-pubkey, nonce, version]`,
* `ack = [recipient-ephemeral-pubkey, nonce, version]`; EIP-8 prefixes the RLP list with a
* one-byte version and appends random padding. Secrets derive as
* `ephemeral-shared = ECDH(ephemeral-priv, remote-ephemeral-pub)`,
* `HKDF(ephemeral-shared, nonce-initiator || nonce-recipient)` split into AES + MAC keys.
*
* Framing: 16-byte header `AES(mac-secret, egress-mac) XOR frame-size`, 16-byte header MAC,
* then AES-CTR frame-data `RLP([capability-message-id, payload...])` plus frame MAC; MACs
* form a SHA-256 egress/ingress chain over ciphertext.
*
* Twist profile: secp256k1/ECIES-KDF map onto bundled X25519 + HKDF-SHA256 + AES-GCM
* (auth/ack box) + AES-CTR (frame stream) + HMAC-SHA256 (MAC chain). RLP schemas,
* message-ids (Hello 0x00, Disconnect 0x01, Ping 0x02, Pong 0x03), disconnect reasons,
* and capability offset (`wire-id = capability-offset + subprotocol-id`) match devp2p.
*/
object Rlpx {
const val AUTH_VERSION = 4
const val PROTOCOL_VERSION = 5
const val NONCE_SIZE = 32
const val PUBKEY_SIZE = 32
const val SIG_SIZE = 64
const val GCM_NONCE_SIZE = 12
const val GCM_TAG_SIZE = 16
const val HEADER_SIZE = 16
const val MAC_SIZE = 16
const val MSG_HELLO = 0x00
const val MSG_DISCONNECT = 0x01
const val MSG_PING = 0x02
const val MSG_PONG = 0x03
// p2p disconnect reasons (`p2p.md`).
const val DISC_REQUESTED = 0x00
const val DISC_TCP_ERROR = 0x01
const val DISC_BAD_PROTOCOL = 0x02
const val DISC_USELESS_PEER = 0x03
const val DISC_TOO_MANY_PEERS = 0x04
const val DISC_ALREADY_CONNECTED = 0x05
const val DISC_INCOMPATIBLE_VERSION = 0x06
const val DISC_NULL_NODE = 0x07
data class AuthPlaintext(
val signature: ByteArray,
val initiatorPubkey: ByteArray,
val nonce: ByteArray,
val version: Int = AUTH_VERSION,
)
data class AckPlaintext(
val recipientEphemeralPubkey: ByteArray,
val nonce: ByteArray,
val version: Int = AUTH_VERSION,
)
data class Secrets(val aes: ByteArray, val mac: ByteArray)
fun encodeAuth(auth: AuthPlaintext): ByteArray {
require(auth.signature.size == SIG_SIZE && auth.initiatorPubkey.size == PUBKEY_SIZE)
require(auth.nonce.size == NONCE_SIZE)
return PRlp.encodeList(
listOf(
PRlp.encodeString(auth.signature),
PRlp.encodeString(auth.initiatorPubkey),
PRlp.encodeString(auth.nonce),
PRlp.encodeInt(auth.version.toLong())))
}
fun decodeAuth(bytes: ByteArray): AuthPlaintext {
val list = (PRlp.decode(bytes) as PRlpItem.List).items
require(list.size == 4)
val sig = (list[0] as PRlpItem.String).bytes
val pub = (list[1] as PRlpItem.String).bytes
val nonce = (list[2] as PRlpItem.String).bytes
require(sig.size == SIG_SIZE && pub.size == PUBKEY_SIZE && nonce.size == NONCE_SIZE)
return AuthPlaintext(sig, pub, nonce, (list[3] as PRlpItem.String).asLong().toInt())
}
fun encodeAck(ack: AckPlaintext): ByteArray {
require(ack.recipientEphemeralPubkey.size == PUBKEY_SIZE && ack.nonce.size == NONCE_SIZE)
return PRlp.encodeList(
listOf(
PRlp.encodeString(ack.recipientEphemeralPubkey),
PRlp.encodeString(ack.nonce),
PRlp.encodeInt(ack.version.toLong())))
}
fun decodeAck(bytes: ByteArray): AckPlaintext {
val list = (PRlp.decode(bytes) as PRlpItem.List).items
require(list.size == 3)
val pub = (list[0] as PRlpItem.String).bytes
val nonce = (list[1] as PRlpItem.String).bytes
require(pub.size == PUBKEY_SIZE && nonce.size == NONCE_SIZE)
return AckPlaintext(pub, nonce, (list[2] as PRlpItem.String).asLong().toInt())
}
// EIP-8 box seal/open and the AES-CTR frame cipher live in transport/RlpxCrypto.kt
// (transport sees p2p + wire + core; p2p must not depend back on transport).
/** `HKDF-SHA256(salt=nonces, ikm=shared)` expanded with info byte; mirrors rlpx secrets. */
fun deriveSecrets(shared: ByteArray, initiatorNonce: ByteArray, recipientNonce: ByteArray): Secrets {
val prk = HmacSha256.digest(initiatorNonce + recipientNonce, shared)
val aes = HmacSha256.digest(prk, byteArrayOf(1))
val mac = HmacSha256.digest(prk, byteArrayOf(2))
return Secrets(aes, mac)
}
fun hkdf(shared: ByteArray, context: ByteArray, size: Int): ByteArray {
val prk = HmacSha256.digest(ByteArray(32), shared)
var out = ByteArray(0)
var counter = 1
while (out.size < size) {
out += HmacSha256.digest(prk, context + counter.toByte())
counter++
}
return out.copyOf(size)
}
// ---- base-protocol RLP bodies (`p2p.md`) ----
fun encodeHello(hello: DevP2pHello): ByteArray =
PRlp.encodeList(
listOf(
PRlp.encodeInt(PROTOCOL_VERSION.toLong()),
PRlp.encodeString(hello.clientId.encodeToByteArray()),
PRlp.encodeList(
hello.capabilities
.sortedWith(compareBy({ it.name }, { it.version }))
.map {
PRlp.encodeList(
listOf(
PRlp.encodeString(it.name.encodeToByteArray()),
PRlp.encodeInt(it.version.toLong())))
}),
PRlp.encodeInt(hello.listenPort.toLong()),
PRlp.encodeString(hello.nodeId.value.encodeToByteArray())))
fun decodeHello(bytes: ByteArray): DevP2pHello {
val list = (PRlp.decode(bytes) as PRlpItem.List).items
require(list.size == 5) { "Hello must have 5 fields" }
require((list[0] as PRlpItem.String).asLong() == PROTOCOL_VERSION.toLong())
val clientId = (list[1] as PRlpItem.String).asText()
val caps =
((list[2] as PRlpItem.List).items.map {
val cap = (it as PRlpItem.List).items
ProtocolCapability((cap[0] as PRlpItem.String).asText(), (cap[1] as PRlpItem.String).asLong().toInt())
}).toSet()
val port = (list[3] as PRlpItem.String).asLong().toInt()
val nodeId = PeerId((list[4] as PRlpItem.String).asText())
return DevP2pHello(clientId, port, nodeId, caps)
}
fun encodeDisconnect(reason: Int): ByteArray =
PRlp.encodeList(listOf(PRlp.encodeInt(reason.toLong())))
fun decodeDisconnect(bytes: ByteArray): Int {
val list = (PRlp.decode(bytes) as PRlpItem.List).items
return if (list.isEmpty()) DISC_REQUESTED else (list[0] as PRlpItem.String).asLong().toInt()
}
fun encodePing(): ByteArray = PRlp.encodeList(emptyList())
fun encodePong(): ByteArray = PRlp.encodeList(emptyList())
/** Subprotocol framing: `RLP([wire-id, payload...])`, `wire-id = offset + sub-id`. */
fun encodeSubprotocolMessage(capabilityOffset: Int, subprotocolId: Int, payload: ByteArray): ByteArray =
PRlp.encodeList(listOf(PRlp.encodeInt((capabilityOffset + subprotocolId).toLong()), PRlp.encodeString(payload)))
fun decodeSubprotocolMessage(bytes: ByteArray, capabilityOffset: Int): Pair<Int, ByteArray> {
val list = (PRlp.decode(bytes) as PRlpItem.List).items
require(list.size == 2)
val wireId = (list[0] as PRlpItem.String).asLong().toInt()
require(wireId >= capabilityOffset) { "Message id below capability offset" }
return Pair(wireId - capabilityOffset, (list[1] as PRlpItem.String).bytes)
}
}
// RlpxFrameCipher: see transport/RlpxCrypto.kt.

View file

@ -0,0 +1,109 @@
package rip.crit.twist.proof
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
interface Proof {
val scheme: String
val subject: Hash
fun verify(): Boolean
}
data class ProofOfStore(override val subject: Hash, val challenge: Hash, val response: Hash) :
Proof {
override val scheme: String = "proof-of-store-v1"
override fun verify(): Boolean =
response == rip.crit.twist.core.Sha256.combine(subject, challenge)
companion object {
fun create(subject: Hash, challenge: Hash): ProofOfStore =
ProofOfStore(subject, challenge, rip.crit.twist.core.Sha256.combine(subject, challenge))
}
}
data class ProofOfStake(
override val subject: Hash,
val validator: String,
val stake: java.math.BigInteger,
val epoch: Long,
) : Proof {
override val scheme: String = "proof-of-stake-v1"
override fun verify(): Boolean = validator.isNotBlank() && stake.signum() > 0 && epoch >= 0
}
data class ProofOfWork(override val subject: Hash, val nonce: Long, val difficulty: Int) : Proof {
override val scheme: String = "proof-of-work-v1"
override fun verify(): Boolean =
nonce >= 0 &&
difficulty in 0..64 &&
rip.crit.twist.core.Sha256.digestUtf8("${subject.value}:$nonce")
.value
.take(difficulty)
.all { it == '0' }
companion object {
fun mine(subject: Hash, difficulty: Int, maxAttempts: Long = 1_000_000): ProofOfWork? {
require(difficulty in 0..64)
for (nonce in 0 until maxAttempts) {
val proof = ProofOfWork(subject, nonce, difficulty)
if (proof.verify()) return proof
}
return null
}
}
}
data class ProofOfAuthority(
override val subject: Hash,
val authority: String,
val signature: Hash,
val allowedAuthorities: Set<String>,
) : Proof {
override val scheme: String = "proof-of-authority-v1"
override fun verify(): Boolean =
authority in allowedAuthorities &&
signature == rip.crit.twist.core.Sha256.digestUtf8("${subject.value}:$authority")
companion object {
fun sign(subject: Hash, authority: String, allowedAuthorities: Set<String>) =
ProofOfAuthority(
subject,
authority,
rip.crit.twist.core.Sha256.digestUtf8("${subject.value}:$authority"),
allowedAuthorities,
)
}
}
class DynamicProofVerifier(private val verifiers: Map<String, (Proof) -> Boolean> = emptyMap()) {
fun verify(proof: Proof): Boolean = verifiers[proof.scheme]?.invoke(proof) ?: proof.verify()
}
/** A value claim bound to a subject. Authenticity is supplied by the signed containing message. */
data class ProofOfValue(
override val subject: Hash,
val beneficiary: String,
val value: java.math.BigInteger,
val commitment: Hash,
) : Proof {
override val scheme: String = "proof-of-value-v1"
override fun verify(): Boolean =
beneficiary.isNotBlank() &&
value.signum() >= 0 &&
commitment == commit(subject, beneficiary, value)
companion object {
fun create(subject: Hash, beneficiary: String, value: java.math.BigInteger): ProofOfValue =
ProofOfValue(subject, beneficiary, value, commit(subject, beneficiary, value))
private fun commit(subject: Hash, beneficiary: String, value: java.math.BigInteger): Hash =
Sha256.digestUtf8("${subject.value}\u0000$beneficiary\u0000$value")
}
}

View file

@ -0,0 +1,102 @@
package rip.crit.twist.reflect
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.time.Clock
import java.time.Instant
import rip.crit.twist.p2p.PeerId
import rip.crit.twist.store.KeyValueStore
data class PeerObservation(
val peer: PeerId,
val addresses: Set<String>,
val neighbors: Set<PeerId>,
val observedAt: Instant,
)
fun interface PeerProbe {
fun inspect(peer: PeerId): PeerObservation?
}
class PeerIndex(private val store: KeyValueStore) {
fun put(observation: PeerObservation) =
store.put(key(observation.peer), ObservationCodec.encode(observation))
fun get(peer: PeerId): PeerObservation? = store.get(key(peer))?.let(ObservationCodec::decode)
private fun key(peer: PeerId) = "reflect:${peer.value}".encodeToByteArray()
}
data class CrawlReport(
val observations: List<PeerObservation>,
val failures: Set<PeerId>,
val truncated: Boolean,
)
/** Breadth-first network crawler with strict node and neighbor limits. */
class NetworkReflector(
private val probe: PeerProbe,
private val index: PeerIndex,
private val clock: Clock = Clock.systemUTC(),
) {
fun crawl(seeds: Collection<PeerId>, maximumPeers: Int = 1_000): CrawlReport {
require(maximumPeers > 0)
val queued = ArrayDeque(seeds.distinct())
val visited = linkedSetOf<PeerId>()
val observations = mutableListOf<PeerObservation>()
val failures = linkedSetOf<PeerId>()
while (queued.isNotEmpty() && visited.size < maximumPeers) {
val peer = queued.removeFirst()
if (!visited.add(peer)) continue
val observation = runCatching { probe.inspect(peer) }.getOrNull()
if (observation == null) {
failures += peer
continue
}
val normalized = observation.copy(observedAt = clock.instant())
index.put(normalized)
observations += normalized
normalized.neighbors.filterNot(visited::contains).forEach(queued::addLast)
}
return CrawlReport(observations, failures, queued.isNotEmpty())
}
}
private object ObservationCodec {
fun encode(value: PeerObservation): ByteArray =
ByteArrayOutputStream().use { buffer ->
DataOutputStream(buffer).use { output ->
output.writeUTF(value.peer.value)
output.writeLong(value.observedAt.toEpochMilli())
output.writeStrings(value.addresses)
output.writeStrings(value.neighbors.map { it.value }.toSet())
}
buffer.toByteArray()
}
fun decode(bytes: ByteArray): PeerObservation? = runCatching {
DataInputStream(ByteArrayInputStream(bytes)).use { input ->
val peer = PeerId(input.readUTF())
val instant = Instant.ofEpochMilli(input.readLong())
val addresses = input.readStrings()
val neighbors = input.readStrings().map(::PeerId).toSet()
require(input.available() == 0)
PeerObservation(peer, addresses, neighbors, instant)
}
}
.getOrNull()
private fun DataOutputStream.writeStrings(values: Set<String>) {
require(values.size <= 10_000)
writeInt(values.size)
values.sorted().forEach(::writeUTF)
}
private fun DataInputStream.readStrings(): Set<String> {
val count = readInt()
require(count in 0..10_000)
return buildSet(count) { repeat(count) { add(readUTF()) } }
}
}

View file

@ -0,0 +1,244 @@
package rip.crit.twist.router
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
import java.math.BigInteger
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
import rip.crit.twist.core.SmartDoc
import rip.crit.twist.ecdsa.Secp256k1Ecdsa
import rip.crit.twist.p2p.NetworkMessage
import rip.crit.twist.p2p.PeerNetwork
import rip.crit.twist.proof.DynamicProofVerifier
import rip.crit.twist.proof.Proof
import rip.crit.twist.proof.ProofOfValue
import rip.crit.twist.proof.ProofOfWork
import rip.crit.twist.store.KeyValueStore
sealed interface ProofPolicy {
fun accepts(proof: Proof?, subject: Hash, worker: Address): Boolean
data object None : ProofPolicy {
override fun accepts(proof: Proof?, subject: Hash, worker: Address) = proof == null
}
data class Work(val minimumDifficulty: Int, val maximumAttempts: Long = 1_000_000) :
ProofPolicy {
init {
require(minimumDifficulty in 0..64)
require(maximumAttempts > 0)
}
override fun accepts(proof: Proof?, subject: Hash, worker: Address): Boolean =
proof is ProofOfWork &&
proof.subject == subject &&
proof.difficulty >= minimumDifficulty &&
proof.verify()
}
data class Value(val minimumValue: BigInteger) : ProofPolicy {
init {
require(minimumValue.signum() >= 0)
}
override fun accepts(proof: Proof?, subject: Hash, worker: Address): Boolean =
proof is ProofOfValue &&
proof.subject == subject &&
proof.beneficiary == worker.value &&
proof.value >= minimumValue &&
proof.verify()
}
class Dynamic(
private val acceptedSchemes: Set<String>,
private val verifier: DynamicProofVerifier = DynamicProofVerifier(),
) : ProofPolicy {
override fun accepts(proof: Proof?, subject: Hash, worker: Address): Boolean =
proof != null &&
proof.subject == subject &&
proof.scheme in acceptedSchemes &&
verifier.verify(proof)
}
}
data class OffchainJob(
val id: Hash,
val requester: Address,
val payload: ByteArray,
val reward: Amount = Amount(BigInteger.ZERO),
val deadlineMillis: Long = Long.MAX_VALUE,
val proofPolicy: ProofPolicy = ProofPolicy.None,
) {
init {
require(deadlineMillis >= 0)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OffchainJob
if (deadlineMillis != other.deadlineMillis) return false
if (id != other.id) return false
if (requester != other.requester) return false
if (!payload.contentEquals(other.payload)) return false
if (reward != other.reward) return false
if (proofPolicy != other.proofPolicy) return false
return true
}
override fun hashCode(): Int {
var result = deadlineMillis.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + requester.hashCode()
result = 31 * result + payload.contentHashCode()
result = 31 * result + reward.hashCode()
result = 31 * result + proofPolicy.hashCode()
return result
}
}
class OffchainResult(
val jobId: Hash,
val worker: Address,
val output: ByteArray,
val proof: Proof?,
val signature: ByteArray,
) {
val outputHash: Hash = Sha256.digest(output)
}
fun interface OffchainComputation {
fun compute(payload: ByteArray): ByteArray
}
interface ResultDistributor {
fun distribute(result: OffchainResult)
}
class NetworkResultDistributor(
private val network: PeerNetwork,
private val store: KeyValueStore? = null,
private val topic: String = "twist/offchain/results/v1",
) : ResultDistributor {
override fun distribute(result: OffchainResult) {
val encoded = ResultEncoding.encode(result)
store?.put(result.jobId.value.encodeToByteArray(), encoded)
network.broadcast(NetworkMessage(topic, encoded, Sha256.digest(encoded)))
}
}
class OffchainWorker(
val address: Address,
private val privateKey: ByteArray,
val publicKey: ByteArray,
private val computation: OffchainComputation,
private val proofFactory: ((ProofPolicy, Hash, Address) -> Proof?)? = null,
private val signer: Secp256k1Ecdsa = Secp256k1Ecdsa(),
) {
init {
require(signer.publicKey(privateKey).contentEquals(publicKey))
}
fun execute(job: OffchainJob): OffchainResult {
val output = computation.compute(job.payload.copyOf())
val outputHash = Sha256.digest(output)
val proof = createProof(job.proofPolicy, outputHash)
val unsigned = ResultEncoding.signingBytes(job.id, address, outputHash, proof)
return OffchainResult(job.id, address, output, proof, signer.sign(unsigned, privateKey))
}
private fun createProof(policy: ProofPolicy, subject: Hash): Proof? =
when (policy) {
ProofPolicy.None -> null
is ProofPolicy.Work ->
ProofOfWork.mine(subject, policy.minimumDifficulty, policy.maximumAttempts)
?: error("Proof of work search exhausted")
is ProofPolicy.Value -> ProofOfValue.create(subject, address.value, policy.minimumValue)
is ProofPolicy.Dynamic ->
requireNotNull(proofFactory?.invoke(policy, subject, address)) {
"Dynamic proof policy requires a worker proof factory"
}
}
}
@SmartDoc(
summary = "Signed off-chain job routing with optional proof validation.",
category = "Routing",
)
class OffchainRouter(
private val distributor: ResultDistributor,
private val clockMillis: () -> Long = System::currentTimeMillis,
private val verifier: Secp256k1Ecdsa = Secp256k1Ecdsa(),
) {
private val workers = linkedMapOf<Address, OffchainWorker>()
fun register(worker: OffchainWorker) {
require(worker.address !in workers) { "Worker already registered" }
workers[worker.address] = worker
}
fun route(job: OffchainJob, worker: Address? = null): OffchainResult {
require(clockMillis() <= job.deadlineMillis) { "Job deadline has passed" }
val selected = worker?.let(workers::get) ?: workers.values.firstOrNull()
requireNotNull(selected) { "No eligible worker registered" }
val result = selected.execute(job)
require(verify(job, result, selected.publicKey)) { "Worker result failed verification" }
distributor.distribute(result)
return result
}
fun verify(job: OffchainJob, result: OffchainResult, publicKey: ByteArray): Boolean {
if (result.jobId != job.id || result.outputHash != Sha256.digest(result.output))
return false
if (!job.proofPolicy.accepts(result.proof, result.outputHash, result.worker)) return false
return verifier.verify(
ResultEncoding.signingBytes(job.id, result.worker, result.outputHash, result.proof),
result.signature,
publicKey,
)
}
fun verify(job: OffchainJob, result: OffchainResult): Boolean =
workers[result.worker]?.let { verify(job, result, it.publicKey) } ?: false
}
object ResultEncoding {
fun signingBytes(jobId: Hash, worker: Address, outputHash: Hash, proof: Proof?): ByteArray =
bytes {
writeUTF(jobId.value)
writeUTF(worker.value)
writeUTF(outputHash.value)
writeUTF(proof?.scheme.orEmpty())
writeUTF(proof?.subject?.value.orEmpty())
writeUTF(proofDetails(proof))
}
fun encode(result: OffchainResult): ByteArray = bytes {
val signing = signingBytes(result.jobId, result.worker, result.outputHash, result.proof)
writeInt(signing.size)
write(signing)
writeInt(result.output.size)
write(result.output)
writeInt(result.signature.size)
write(result.signature)
}
private fun proofDetails(proof: Proof?): String =
when (proof) {
null -> ""
is ProofOfWork -> "${proof.nonce}:${proof.difficulty}"
is ProofOfValue -> "${proof.beneficiary}:${proof.value}:${proof.commitment.value}"
else -> proof.toString()
}
private fun bytes(write: DataOutputStream.() -> Unit): ByteArray =
ByteArrayOutputStream().use { buffer ->
DataOutputStream(buffer).use { it.write() }
buffer.toByteArray()
}
}

View file

@ -0,0 +1,7 @@
package rip.crit.twist.rsa
interface Rsa {
fun encrypt(message: ByteArray, publicKey: ByteArray): ByteArray
fun decrypt(ciphertext: ByteArray, privateKey: ByteArray): ByteArray
}

View file

@ -0,0 +1,122 @@
package rip.crit.twist.rsa
import java.math.BigInteger
import rip.crit.twist.core.Sha256
data class RsaPublicKey(val modulus: BigInteger, val exponent: BigInteger)
data class RsaPrivateKey(val modulus: BigInteger, val exponent: BigInteger)
data class RsaKeyPair(
val publicKey: RsaPublicKey,
val privateKey: RsaPrivateKey,
) {
companion object {
fun fromPrimes(
p: BigInteger,
q: BigInteger,
e: BigInteger = BigInteger.valueOf(65537),
): RsaKeyPair {
require(p.isProbablePrime(100) && q.isProbablePrime(100) && p != q)
val n = p * q
val lambda =
(p - BigInteger.ONE)
.multiply(q - BigInteger.ONE)
.divide((p - BigInteger.ONE).gcd(q - BigInteger.ONE))
require(e.gcd(lambda) == BigInteger.ONE)
return RsaKeyPair(
RsaPublicKey(n, e),
RsaPrivateKey(n, e.modInverse(lambda)),
)
}
}
}
/** RFC 8017 RSAES-OAEP primitive with caller-supplied randomness. */
object RsaOaep {
fun encrypt(
message: ByteArray,
key: RsaPublicKey,
seed: ByteArray,
label: ByteArray = byteArrayOf(),
): ByteArray {
val k = (key.modulus.bitLength() + 7) / 8
val h = Sha256.bytes(label)
require(seed.size == 32 && message.size <= k - 66)
val db = h + ByteArray(k - message.size - 66) + byteArrayOf(1) + message
val dbMask = mgf(seed, k - 33)
val maskedDb = xor(db, dbMask)
val seedMask = mgf(maskedDb, 32)
return i2osp(
BigInteger(
1,
byteArrayOf(0) + xor(seed, seedMask) + maskedDb,
)
.modPow(key.exponent, key.modulus),
k,
)
}
fun decrypt(
ciphertext: ByteArray,
key: RsaPrivateKey,
label: ByteArray = byteArrayOf(),
): ByteArray {
val k = (key.modulus.bitLength() + 7) / 8
require(ciphertext.size == k)
val encoded = i2osp(BigInteger(1, ciphertext).modPow(key.exponent, key.modulus), k)
require(encoded[0].toInt() == 0)
val maskedSeed = encoded.copyOfRange(1, 33)
val maskedDb = encoded.copyOfRange(33, k)
val seed = xor(maskedSeed, mgf(maskedDb, 32))
val db = xor(maskedDb, mgf(seed, k - 33))
require(db.copyOfRange(0, 32).contentEquals(Sha256.bytes(label)))
var index = 32
while (index < db.size && db[index].toInt() == 0) index++
require(index < db.size && db[index].toInt() == 1)
return db.copyOfRange(
index + 1,
db.size,
)
}
private fun mgf(seed: ByteArray, length: Int): ByteArray {
val output = ByteArray(length)
var offset = 0
var counter = 0
while (offset < length) {
val c =
byteArrayOf(
(counter ushr 24).toByte(),
(counter ushr 16).toByte(),
(counter ushr 8).toByte(),
counter.toByte(),
)
val hash = Sha256.bytes(seed + c)
hash.copyInto(
output,
offset,
0,
minOf(hash.size, length - offset),
)
offset += hash.size
counter++
}
return output
}
private fun xor(a: ByteArray, b: ByteArray) =
ByteArray(a.size) { (a[it].toInt() xor b[it].toInt()).toByte() }
private fun i2osp(value: BigInteger, size: Int): ByteArray {
val source = value.toByteArray()
require(source.size <= size + 1)
return ByteArray(size).also {
source.copyInto(
it,
(size - source.size).coerceAtLeast(0),
(source.size - size).coerceAtLeast(0),
)
}
}
}

View file

@ -0,0 +1,96 @@
package rip.crit.twist.smartdoc
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.extension
import kotlin.io.path.name
data class SmartDocEntry(
val name: String,
val packageName: String,
val category: String,
val summary: String,
val stability: String,
val source: String,
)
object SmartDocGenerator {
fun generate(sourceRoot: Path, output: Path): List<SmartDocEntry> {
val root =
if (Files.isDirectory(sourceRoot)) {
sourceRoot
} else if (Files.isDirectory(Path.of("..").resolve(sourceRoot))) {
Path.of("..").resolve(sourceRoot).normalize()
} else if (Files.isDirectory(Path.of("../..").resolve(sourceRoot))) {
Path.of("../..").resolve(sourceRoot).normalize()
} else {
sourceRoot
}
require(Files.isDirectory(root)) { "Source directory does not exist: $sourceRoot (resolved: $root)" }
val entries =
Files.walk(root).use { paths ->
paths
.filter { Files.isRegularFile(it) && it.extension == "kt" }
.flatMap { path -> parse(path, root).stream() }
.sorted(compareBy(SmartDocEntry::category, SmartDocEntry::name))
.toList()
}
Files.createDirectories(output)
Files.writeString(output.resolve("index.html"), page(entries))
Files.writeString(output.resolve("api.json"), json(entries))
return entries
}
private fun parse(path: Path, root: Path): List<SmartDocEntry> {
val source = Files.readString(path)
val packageName = PACKAGE.find(source)?.groupValues?.get(1).orEmpty()
return ANNOTATION.findAll(source)
.mapNotNull { match ->
val tail = source.substring(match.range.last + 1)
val declaration = DECLARATION.find(tail) ?: return@mapNotNull null
SmartDocEntry(
declaration.groupValues[2],
packageName,
match.groupValues[2],
match.groupValues[1],
match.groupValues[3].ifBlank { "experimental" },
root.relativize(path).toString(),
)
}
.toList()
}
private fun page(entries: List<SmartDocEntry>): String =
"""
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Twist SmartDoc API</title><style>
body{font:16px system-ui,sans-serif;max-width:960px;margin:3rem auto;padding:0 1rem;color:#18212f;background:#f8fafc}
h1{color:#5b21b6}.entry{background:white;border:1px solid #dbe3ef;border-radius:8px;padding:1rem;margin:.75rem 0}
code{color:#0f766e}.tag{float:right;color:#475569;font-size:.85rem}
</style></head><body><h1>Twist SmartDoc API</h1><p>${entries.size} documented public APIs.</p>
${entries.joinToString("\n") { entry -> "<article class=\"entry\"><span class=\"tag\">${escape(entry.category)} · ${escape(entry.stability)}</span><h2>${escape(entry.name)}</h2><code>${escape(entry.packageName)}</code><p>${escape(entry.summary)}</p><small>${escape(entry.source)}</small></article>" }}
</body></html>
"""
.trimIndent()
private fun json(entries: List<SmartDocEntry>): String =
entries.joinToString(",", "[", "]") {
"{\"name\":\"${escape(it.name)}\",\"package\":\"${escape(it.packageName)}\",\"category\":\"${escape(it.category)}\",\"summary\":\"${escape(it.summary)}\",\"stability\":\"${escape(it.stability)}\",\"source\":\"${escape(it.source)}\"}"
}
private fun escape(value: String): String =
value
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
private val PACKAGE = Regex("(?m)^package\\s+([\\w.]+)")
private val ANNOTATION =
Regex(
"@SmartDoc\\(\\s*summary\\s*=\\s*\"([^\"]+)\"\\s*,\\s*category\\s*=\\s*\"([^\"]+)\"(?:\\s*,\\s*stability\\s*=\\s*\"([^\"]+)\")?,?\\s*\\)"
)
private val DECLARATION =
Regex("(?:public\\s+)?(class|object|fun|interface)\\s+([A-Za-z_][A-Za-z0-9_]*)")
}

View file

@ -0,0 +1,25 @@
package rip.crit.twist.stake
import java.math.BigInteger
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
class InMemoryStaking : Staking {
private val delegations = ConcurrentHashMap<Pair<Address, Address>, Amount>()
override fun stake(delegator: Address, validator: Address, amount: Amount) {
require(amount.value > BigInteger.ZERO)
delegations.compute(delegator to validator) { _, current ->
Amount((current?.value ?: BigInteger.ZERO) + amount.value)
}
}
override fun votingPower(validator: Address): Amount =
Amount(
delegations
.filterKeys { it.second == validator }
.values
.fold(BigInteger.ZERO) { total, amount -> total + amount.value }
)
}

View file

@ -0,0 +1,25 @@
package rip.crit.twist.stake
import java.math.BigInteger
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.store.KeyValueStore
class PersistentStaking(private val store: KeyValueStore) : Staking {
override fun stake(delegator: Address, validator: Address, amount: Amount) {
require(amount.value > BigInteger.ZERO)
val key = delegationKey(delegator, validator)
val current = store.get(key)?.let(::BigInteger) ?: BigInteger.ZERO
store.put(key, (current + amount.value).toByteArray())
val total = store.get(totalKey(validator))?.let(::BigInteger) ?: BigInteger.ZERO
store.put(totalKey(validator), (total + amount.value).toByteArray())
}
override fun votingPower(validator: Address): Amount =
Amount(store.get(totalKey(validator))?.let(::BigInteger) ?: BigInteger.ZERO)
private fun delegationKey(delegator: Address, validator: Address) =
"delegation:${delegator.value}:${validator.value}".encodeToByteArray()
private fun totalKey(validator: Address) = "validator:${validator.value}".encodeToByteArray()
}

View file

@ -0,0 +1,10 @@
package rip.crit.twist.stake
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
interface Staking {
fun stake(delegator: Address, validator: Address, amount: Amount)
fun votingPower(validator: Address): Amount
}

View file

@ -0,0 +1,25 @@
package rip.crit.twist.standards
import java.math.BigInteger
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
class InMemoryToken(
override val symbol: String,
initialBalances: Map<Address, Amount> = emptyMap(),
) : TokenStandard {
private val balances = ConcurrentHashMap(initialBalances)
override fun balanceOf(owner: Address): Amount = balances[owner] ?: Amount(BigInteger.ZERO)
override fun transfer(from: Address, to: Address, amount: Amount): Boolean =
synchronized(this) {
if (amount.value == BigInteger.ZERO || balanceOf(from).value < amount.value) false
else {
balances[from] = Amount(balanceOf(from).value - amount.value)
balances[to] = Amount(balanceOf(to).value + amount.value)
true
}
}
}

View file

@ -0,0 +1,28 @@
package rip.crit.twist.standards
import java.math.BigInteger
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.store.KeyValueStore
class PersistentToken(override val symbol: String, private val store: KeyValueStore) :
TokenStandard {
override fun balanceOf(owner: Address): Amount =
Amount(store.get(key(owner))?.let(::BigInteger) ?: BigInteger.ZERO)
override fun transfer(from: Address, to: Address, amount: Amount): Boolean =
synchronized(this) {
val source = balanceOf(from).value
if (amount.value == BigInteger.ZERO || source < amount.value) return false
store.put(key(from), (source - amount.value).toByteArray())
store.put(key(to), (balanceOf(to).value + amount.value).toByteArray())
true
}
fun mint(to: Address, amount: Amount) =
synchronized(this) {
store.put(key(to), (balanceOf(to).value + amount.value).toByteArray())
}
private fun key(owner: Address) = "$symbol:${owner.value}".encodeToByteArray()
}

View file

@ -0,0 +1,12 @@
package rip.crit.twist.standards
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
interface TokenStandard {
val symbol: String
fun balanceOf(owner: Address): Amount
fun transfer(from: Address, to: Address, amount: Amount): Boolean
}

View file

@ -0,0 +1,71 @@
package rip.crit.twist.state
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.util.Base64
import java.util.concurrent.ConcurrentHashMap
/** File-backed node state rooted at a caller-selected folder. */
class FileNodeState(
private val root: Path = Paths.get(".twist", "nodes"),
) : NodeState {
private val nodes = ConcurrentHashMap<String, Node>()
init {
Files.createDirectories(root)
Files.list(root).use { files ->
files.filter(Files::isRegularFile).forEach { file ->
runCatching {
ObjectInputStream(Files.newInputStream(file)).use { input ->
nodes[file.fileName.toString()] = input.readObject() as Node
}
}
}
}
}
override fun getNode(key: String): Node? = nodes[safe(key)]
override fun putNode(key: String, node: Node) {
val filename = safe(key)
val target = root.resolve(filename)
val temporary = Files.createTempFile(root, "node-", ".tmp")
try {
ObjectOutputStream(Files.newOutputStream(temporary)).use { it.writeObject(node) }
Files.move(
temporary,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE,
)
nodes[filename] = node
} finally {
Files.deleteIfExists(temporary)
}
}
override fun rootHash(): String {
val digest = MessageDigest.getInstance("SHA-256")
Files.list(root).use { files ->
files.filter(Files::isRegularFile).sorted().forEach { file ->
digest.update(file.fileName.toString().encodeToByteArray())
digest.update(Files.readAllBytes(file))
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}
override fun snapshot(): StateSnapshot = FileNodeSnapshot(nodes.toMap())
private fun safe(value: String): String =
Base64.getUrlEncoder().withoutPadding().encodeToString(value.encodeToByteArray())
}
class FileNodeSnapshot internal constructor(internal val nodes: Map<String, Node>) : StateSnapshot {
override fun close() = Unit
}

View file

@ -0,0 +1,116 @@
package rip.crit.twist.state
import java.math.BigInteger
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.util.Base64
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
/** File-backed account and contract state rooted at a caller-selected folder. */
class FileWorldState(private val root: Path) : ContractState {
private val accounts = root.resolve("accounts")
private val storage = root.resolve("storage")
init {
Files.createDirectories(accounts)
Files.createDirectories(storage)
}
override fun account(address: Address): Account? {
val lines =
path(accounts, address.value).takeIf(Files::exists)?.let(Files::readAllLines)
?: return null
return Account(
lines[0].toLong(),
Amount(BigInteger(lines[1])),
Base64.getDecoder().decode(lines[2]),
)
}
override fun putAccount(address: Address, account: Account) =
write(
path(accounts, address.value),
listOf(
account.nonce.toString(),
account.balance.value.toString(),
Base64.getEncoder().encodeToString(account.code),
),
)
override fun deleteAccount(address: Address) {
Files.deleteIfExists(path(accounts, address.value))
deleteStorage(address)
}
override fun storage(contract: Address, key: Hash): ByteArray? =
path(storage.resolve(safe(contract.value)), key.value)
.takeIf(Files::exists)
?.let(Files::readAllBytes)
override fun putStorage(contract: Address, key: Hash, value: ByteArray) {
val directory = storage.resolve(safe(contract.value))
Files.createDirectories(directory)
writeBytes(path(directory, key.value), value)
}
override fun deleteStorage(contract: Address) {
val directory = storage.resolve(safe(contract.value))
if (!Files.exists(directory)) return
Files.walk(directory).use { paths ->
paths.sorted(Comparator.reverseOrder()).forEach(Files::deleteIfExists)
}
}
override fun rootHash(): Hash {
val content =
Files.walk(root).use { paths ->
paths
.filter(Files::isRegularFile)
.sorted()
.flatMap { path ->
java.util.stream.Stream.of(
root.relativize(path).toString().encodeToByteArray(),
Files.readAllBytes(path),
)
}
.reduce(byteArrayOf()) { a, b -> a + b }
}
return Sha256.digest(content)
}
override fun snapshot(): StateSnapshot =
object : StateSnapshot {
override fun close() = Unit
}
private fun path(directory: Path, key: String) = directory.resolve(safe(key))
private fun safe(value: String) =
Base64.getUrlEncoder().withoutPadding().encodeToString(value.encodeToByteArray())
private fun write(target: Path, lines: List<String>) {
val temporary = Files.createTempFile(target.parent, "state-", ".tmp")
Files.write(temporary, lines)
Files.move(
temporary,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE,
)
}
private fun writeBytes(target: Path, value: ByteArray) {
val temporary = Files.createTempFile(target.parent, "slot-", ".tmp")
Files.write(temporary, value)
Files.move(
temporary,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE,
)
}
}

View file

@ -0,0 +1,55 @@
package rip.crit.twist.state
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.core.Sha256
class InMemoryNodeState : NodeState {
private val nodes = ConcurrentHashMap<String, Node>()
override fun getNode(key: String): Node? = nodes[key]?.deepCopy()
override fun putNode(key: String, node: Node) {
require(key.isNotBlank())
nodes[key] = node.deepCopy()
}
override fun rootHash(): String {
val bytes =
nodes.entries
.sortedBy { it.key }
.fold(byteArrayOf()) { output, (key, node) ->
output + key.encodeToByteArray() + canonical(node)
}
return Sha256.digest(bytes).value
}
override fun snapshot(): StateSnapshot = NodeSnapshot(nodes.mapValues { it.value.deepCopy() })
private fun canonical(node: Node): ByteArray {
val storage =
node.storage.entries
.sortedBy { it.key }
.fold(byteArrayOf()) { output, entry ->
output + entry.key.encodeToByteArray() + entry.value
}
return node.accounts.entries
.sortedBy { it.key }
.fold(storage) { output, entry ->
output +
entry.key.encodeToByteArray() +
entry.value.balance.toString().encodeToByteArray() +
entry.value.nonce.toString().encodeToByteArray() +
entry.value.code
}
}
private fun Node.deepCopy() =
copy(
storage = storage.mapValues { it.value.copyOf() },
accounts = accounts.mapValues { it.value.copy(code = it.value.code.copyOf()) },
)
}
class NodeSnapshot internal constructor(internal val nodes: Map<String, Node>) : StateSnapshot {
override fun close() = Unit
}

View file

@ -0,0 +1,59 @@
package rip.crit.twist.state
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.core.Address
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
class InMemoryWorldState : ContractState {
private val accounts = ConcurrentHashMap<Address, Account>()
private val storage = ConcurrentHashMap<Pair<Address, Hash>, ByteArray>()
override fun account(address: Address): Account? =
accounts[address]?.let { it.copy(code = it.code.copyOf()) }
override fun putAccount(address: Address, account: Account) {
accounts[address] = account.copy(code = account.code.copyOf())
}
override fun deleteAccount(address: Address) {
accounts.remove(address)
deleteStorage(address)
}
override fun rootHash(): Hash =
Sha256.digestUtf8(
accounts.entries
.sortedBy { it.key.value }
.joinToString("|") { (address, account) ->
"${address.value},${account.nonce},${account.balance.value},${
account.code.joinToString("") {
"%02x".format(
it
)
}
}"
}
)
override fun snapshot(): StateSnapshot =
InMemorySnapshot(
accounts.mapValues { (_, account) -> account.copy(code = account.code.copyOf()) }
)
override fun storage(contract: Address, key: Hash): ByteArray? =
storage[contract to key]?.copyOf()
override fun putStorage(contract: Address, key: Hash, value: ByteArray) {
storage[contract to key] = value.copyOf()
}
override fun deleteStorage(contract: Address) {
storage.keys.removeIf { it.first == contract }
}
}
class InMemorySnapshot internal constructor(internal val accounts: Map<Address, Account>) :
StateSnapshot {
override fun close() = Unit
}

View file

@ -0,0 +1,42 @@
package rip.crit.twist.state
data class Node(
val storage: Map<String, ByteArray> = emptyMap(),
val accounts: Map<String, AccountState> = emptyMap(),
)
data class AccountState(
val balance: Long = 0,
val nonce: Long = 0,
val code: ByteArray = ByteArray(0),
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AccountState
if (balance != other.balance) return false
if (nonce != other.nonce) return false
if (!code.contentEquals(other.code)) return false
return true
}
override fun hashCode(): Int {
var result = balance.hashCode()
result = 31 * result + nonce.hashCode()
result = 31 * result + code.contentHashCode()
return result
}
}
interface NodeState {
fun getNode(key: String): Node?
fun putNode(key: String, node: Node)
fun rootHash(): String
fun snapshot(): StateSnapshot
}

View file

@ -0,0 +1,51 @@
package rip.crit.twist.state
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
data class Account(val nonce: Long = 0, val balance: Amount, val code: ByteArray = byteArrayOf()) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Account
if (nonce != other.nonce) return false
if (balance != other.balance) return false
if (!code.contentEquals(other.code)) return false
return true
}
override fun hashCode(): Int {
var result = nonce.hashCode()
result = 31 * result + balance.hashCode()
result = 31 * result + code.contentHashCode()
return result
}
}
interface WorldState {
fun account(address: Address): Account?
fun putAccount(address: Address, account: Account)
fun deleteAccount(address: Address)
fun rootHash(): Hash
fun snapshot(): StateSnapshot
}
interface ContractState : WorldState {
fun storage(contract: Address, key: Hash): ByteArray?
fun putStorage(contract: Address, key: Hash, value: ByteArray)
fun deleteStorage(contract: Address)
}
interface StateSnapshot : AutoCloseable {
override fun close()
}

View file

@ -0,0 +1,108 @@
package rip.crit.twist.store
import java.math.BigInteger
import java.nio.file.Files
import java.nio.file.Path
import java.util.Base64
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Block
import rip.crit.twist.core.BlockHeader
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Transaction
/** File-per-key store with atomic replacement and defensive copies. */
class FileKeyValueStore(private val root: Path) : KeyValueStore {
init {
Files.createDirectories(root)
}
override fun get(key: ByteArray): ByteArray? =
root.resolve(key.encode()).takeIf(Files::exists)?.let(Files::readAllBytes)
override fun put(key: ByteArray, value: ByteArray) {
val target = root.resolve(key.encode())
val temporary = Files.createTempFile(root, "put-", ".tmp")
Files.write(temporary, value)
Files.move(
temporary,
target,
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
java.nio.file.StandardCopyOption.ATOMIC_MOVE,
)
}
override fun delete(key: ByteArray) {
Files.deleteIfExists(root.resolve(key.encode()))
}
private fun ByteArray.encode(): String =
Base64.getUrlEncoder().withoutPadding().encodeToString(this)
}
class FileBlockStore(private val root: Path) : BlockStore {
init {
Files.createDirectories(root)
}
override fun get(hash: Hash): Block? =
root.resolve("${hash.value}.block").takeIf(Files::exists)?.let {
decode(Files.readAllLines(it))
}
override fun put(block: Block) {
val file = root.resolve("${block.hash.value}.block")
val temporary = Files.createTempFile(root, "block-", ".tmp")
Files.write(temporary, encode(block))
Files.move(
temporary,
file,
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
java.nio.file.StandardCopyOption.ATOMIC_MOVE,
)
}
private fun encode(block: Block): List<String> = buildList {
add(block.hash.value)
add(block.header.parentHash.value)
add(block.header.stateRoot.value)
add(block.header.height.toString())
add(block.header.timestampMillis.toString())
add(block.transactions.size.toString())
block.transactions.forEach {
add(
listOf(
it.id.value,
it.sender.value,
it.recipient?.value.orEmpty(),
it.nonce,
it.value.value,
Base64.getEncoder().encodeToString(it.payload),
)
.joinToString("\t")
)
}
}
private fun decode(lines: List<String>): Block? = runCatching {
require(lines.size >= 6)
val header =
BlockHeader(Hash(lines[1]), Hash(lines[2]), lines[3].toLong(), lines[4].toLong())
val count = lines[5].toInt()
require(lines.size == 6 + count)
val transactions =
lines.drop(6).map { line ->
val f = line.split("\t", limit = 6)
Transaction(
Hash(f[0]),
Address(f[1]),
f[2].takeIf(String::isNotEmpty)?.let(::Address),
f[3].toLong(),
Amount(BigInteger(f[4])),
Base64.getDecoder().decode(f[5]),
)
}
Block(header, transactions, Hash(lines[0]))
}
.getOrNull()
}

View file

@ -0,0 +1,31 @@
package rip.crit.twist.store
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.core.Block
import rip.crit.twist.core.Hash
class InMemoryBlockStore : BlockStore {
private val blocks = ConcurrentHashMap<Hash, Block>()
override fun get(hash: Hash): Block? = blocks[hash]
override fun put(block: Block) {
blocks[block.hash] = block
}
}
class InMemoryKeyValueStore : KeyValueStore {
private val values = ConcurrentHashMap<String, ByteArray>()
override fun get(key: ByteArray): ByteArray? = values[key.toKey()]?.copyOf()
override fun put(key: ByteArray, value: ByteArray) {
values[key.toKey()] = value.copyOf()
}
override fun delete(key: ByteArray) {
values.remove(key.toKey())
}
private fun ByteArray.toKey(): String = joinToString("") { "%02x".format(it) }
}

View file

@ -0,0 +1,18 @@
package rip.crit.twist.store
import rip.crit.twist.core.Block
import rip.crit.twist.core.Hash
interface BlockStore {
fun get(hash: Hash): Block?
fun put(block: Block)
}
interface KeyValueStore {
fun get(key: ByteArray): ByteArray?
fun put(key: ByteArray, value: ByteArray)
fun delete(key: ByteArray)
}

View file

@ -0,0 +1,227 @@
package rip.crit.twist.transport
import rip.crit.twist.core.SmartDoc
/** FIPS 197 AES and SP 800-38D GCM implementation. */
class Aes(private val key: ByteArray) {
private val rounds = key.size / 4 + 6
private val schedule: IntArray
init {
require(key.size in setOf(16, 24, 32))
schedule = expandKey(key)
}
fun encryptBlock(input: ByteArray): ByteArray {
require(input.size == 16)
var state = input.copyOf()
addRoundKey(state, 0)
for (round in 1 until rounds) {
subBytes(state)
state = shiftRows(state)
mixColumns(state)
addRoundKey(state, round)
}
subBytes(state)
state = shiftRows(state)
addRoundKey(state, rounds)
return state
}
private fun expandKey(key: ByteArray): IntArray {
val nk = key.size / 4
val words = IntArray(4 * (rounds + 1))
for (i in 0 until nk) words[i] =
((key[4 * i].toInt() and 255) shl 24) or
((key[4 * i + 1].toInt() and 255) shl 16) or
((key[4 * i + 2].toInt() and 255) shl 8) or
(key[4 * i + 3].toInt() and 255)
var rcon = 1
for (i in nk until words.size) {
var temp = words[i - 1]
if (i % nk == 0) {
temp = subWord(temp.rotateLeft(8)) xor (rcon shl 24)
rcon = multiply(rcon, 2)
} else if (nk > 6 && i % nk == 4) temp = subWord(temp)
words[i] = words[i - nk] xor temp
}
return words
}
private fun subWord(word: Int): Int =
(sbox(word ushr 24) shl 24) or
(sbox(word ushr 16) shl 16) or
(sbox(word ushr 8) shl 8) or
sbox(word)
private fun subBytes(state: ByteArray) {
for (i in state.indices) state[i] = sbox(state[i].toInt()).toByte()
}
private fun shiftRows(s: ByteArray): ByteArray =
ByteArray(16).also { out ->
for (r in 0..3) for (c in 0..3) out[r + 4 * c] = s[r + 4 * ((c + r) % 4)]
}
private fun mixColumns(s: ByteArray) {
for (c in 0..3) {
val i = 4 * c
val a = IntArray(4) { s[i + it].toInt() and 255 }
s[i] = (multiply(a[0], 2) xor multiply(a[1], 3) xor a[2] xor a[3]).toByte()
s[i + 1] = (a[0] xor multiply(a[1], 2) xor multiply(a[2], 3) xor a[3]).toByte()
s[i + 2] = (a[0] xor a[1] xor multiply(a[2], 2) xor multiply(a[3], 3)).toByte()
s[i + 3] = (multiply(a[0], 3) xor a[1] xor a[2] xor multiply(a[3], 2)).toByte()
}
}
private fun addRoundKey(s: ByteArray, round: Int) {
for (c in 0..3) {
val w = schedule[round * 4 + c]
for (r in 0..3) s[4 * c + r] = (s[4 * c + r].toInt() xor (w ushr (24 - 8 * r))).toByte()
}
}
private fun sbox(value: Int): Int {
val x = value and 255
val inverse = if (x == 0) 0 else power(x, 254)
return (inverse xor
inverse.rotateByte(1) xor
inverse.rotateByte(2) xor
inverse.rotateByte(3) xor
inverse.rotateByte(4) xor
0x63) and 255
}
private fun Int.rotateByte(bits: Int): Int = ((this shl bits) or (this ushr (8 - bits))) and 255
private fun power(value: Int, exponent: Int): Int {
var base = value
var power = exponent
var result = 1
while (power > 0) {
if (power and 1 != 0) result = multiply(result, base)
base = multiply(base, base)
power = power ushr 1
}
return result
}
private fun multiply(left: Int, right: Int): Int {
var a = left
var b = right
var result = 0
repeat(8) {
if (b and 1 != 0) result = result xor a
a = ((a shl 1) xor if (a and 0x80 != 0) 0x11b else 0) and 255
b = b ushr 1
}
return result
}
}
data class GcmCiphertext(val ciphertext: ByteArray, val tag: ByteArray)
@SmartDoc(
summary = "AES-GCM authenticated encryption with explicit nonces.",
category = "Cryptography",
)
class AesGcm(key: ByteArray) {
private val aes = Aes(key)
fun encrypt(
nonce: ByteArray,
plaintext: ByteArray,
aad: ByteArray = byteArrayOf(),
): GcmCiphertext {
require(nonce.size == 12)
val h = aes.encryptBlock(ByteArray(16))
val j0 = nonce + byteArrayOf(0, 0, 0, 1)
val ciphertext = ctr(j0, plaintext)
val tag = xor(aes.encryptBlock(j0), ghash(h, aad, ciphertext))
return GcmCiphertext(ciphertext, tag)
}
fun decrypt(
nonce: ByteArray,
ciphertext: ByteArray,
tag: ByteArray,
aad: ByteArray = byteArrayOf(),
): ByteArray {
require(tag.size == 16)
val result = encryptTag(nonce, ciphertext, aad)
require(constantEquals(tag, result)) { "GCM authentication failed" }
return ctr(nonce + byteArrayOf(0, 0, 0, 1), ciphertext)
}
private fun encryptTag(nonce: ByteArray, ciphertext: ByteArray, aad: ByteArray): ByteArray {
require(nonce.size == 12)
val h = aes.encryptBlock(ByteArray(16))
return xor(aes.encryptBlock(nonce + byteArrayOf(0, 0, 0, 1)), ghash(h, aad, ciphertext))
}
private fun ctr(j0: ByteArray, input: ByteArray): ByteArray {
val counter = j0.copyOf()
val out = ByteArray(input.size)
var offset = 0
while (offset < input.size) {
increment(counter)
val stream = aes.encryptBlock(counter)
val count = minOf(16, input.size - offset)
for (i in 0 until count) out[offset + i] =
(input[offset + i].toInt() xor stream[i].toInt()).toByte()
offset += count
}
return out
}
private fun increment(counter: ByteArray) {
for (i in 15 downTo 12) {
counter[i] = (counter[i] + 1).toByte()
if (counter[i].toInt() != 0) break
}
}
private fun ghash(h: ByteArray, aad: ByteArray, ciphertext: ByteArray): ByteArray {
var y = ByteArray(16)
for (block in blocks(aad) + blocks(ciphertext)) y = multiplyGf(xor(y, block), h)
val lengths = ByteArray(16)
writeLong(lengths, 0, aad.size.toLong() * 8)
writeLong(lengths, 8, ciphertext.size.toLong() * 8)
return multiplyGf(xor(y, lengths), h)
}
private fun blocks(data: ByteArray): List<ByteArray> =
data.asList().chunked(16).map { chunk ->
ByteArray(16).also { out -> chunk.forEachIndexed { i, b -> out[i] = b } }
}
private fun multiplyGf(x: ByteArray, y: ByteArray): ByteArray {
var z = ByteArray(16)
val v = y.copyOf()
for (i in 0 until 128) {
if ((x[i / 8].toInt() and (1 shl (7 - i % 8))) != 0)
for (j in 0..15) z[j] = (z[j].toInt() xor v[j].toInt()).toByte()
val lsb = v[15].toInt() and 1
for (j in 15 downTo 0) v[j] =
(((v[j].toInt() and 255) ushr 1) or
if (j > 0) ((v[j - 1].toInt() and 1) shl 7) else 0)
.toByte()
if (lsb != 0) v[0] = (v[0].toInt() xor 0xe1).toByte()
}
return z
}
private fun xor(a: ByteArray, b: ByteArray) =
ByteArray(a.size) { (a[it].toInt() xor b[it].toInt()).toByte() }
private fun writeLong(out: ByteArray, offset: Int, value: Long) {
for (i in 0..7) out[offset + i] = (value ushr (56 - 8 * i)).toByte()
}
private fun constantEquals(a: ByteArray, b: ByteArray): Boolean {
if (a.size != b.size) return false
var d = 0
for (i in a.indices) d = d or (a[i].toInt() xor b[i].toInt())
return d == 0
}
}

View file

@ -0,0 +1,67 @@
package rip.crit.twist.transport
import java.io.DataInputStream
import java.io.DataOutputStream
import java.net.Socket
import kotlin.random.Random
import rip.crit.twist.core.HmacSha256
import rip.crit.twist.wire.FrameCodec
import rip.crit.twist.wire.PacketCodec
import rip.crit.twist.wire.WirePacket
data class X25519KeyPair(val privateKey: ByteArray, val publicKey: ByteArray)
/** X25519 key agreement with HKDF-SHA256 and AES-GCM. */
class X25519Session private constructor(private val key: ByteArray) {
fun encrypt(plain: ByteArray, nonce: ByteArray): ByteArray {
val encrypted = AesGcm(key).encrypt(nonce, plain)
return encrypted.ciphertext + encrypted.tag
}
fun decrypt(ciphertext: ByteArray, nonce: ByteArray): ByteArray {
require(ciphertext.size >= 16)
return AesGcm(key)
.decrypt(
nonce,
ciphertext.copyOfRange(0, ciphertext.size - 16),
ciphertext.copyOfRange(ciphertext.size - 16, ciphertext.size),
)
}
companion object {
fun generateKeyPair(random: Random = Random.Default): X25519KeyPair {
val privateKey = random.nextBytes(32)
return X25519KeyPair(privateKey, X25519.publicKey(privateKey))
}
fun establish(
privateKey: ByteArray,
remotePublicKey: ByteArray,
context: ByteArray = "twist-v1".encodeToByteArray(),
): X25519Session {
val secret = X25519.sharedSecret(privateKey, remotePublicKey)
val prk = HmacSha256.digest(ByteArray(32), secret)
return X25519Session(HmacSha256.digest(prk, context + byteArrayOf(1)))
}
}
}
/** Blocking TCP transport, suitable for adapters and integration tests. */
class TcpPacketTransport(private val socket: Socket) : AutoCloseable {
private val input = DataInputStream(socket.getInputStream())
private val output = DataOutputStream(socket.getOutputStream())
fun send(packet: WirePacket) {
val frame = FrameCodec.encode(PacketCodec.encode(packet))
output.write(frame)
output.flush()
}
fun receive(): WirePacket {
val size = input.readInt()
require(size in 0..FrameCodec.MAX_FRAME_SIZE)
return PacketCodec.decode(input.readNBytes(size))
}
override fun close() = socket.close()
}

View file

@ -0,0 +1,122 @@
package rip.crit.twist.transport
import java.io.DataInputStream
import java.io.DataOutputStream
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import rip.crit.twist.core.SmartDoc
import rip.crit.twist.p2p.EnvelopeAdapter
import rip.crit.twist.p2p.NetworkAdapter
import rip.crit.twist.p2p.NetworkAdapterKind
import rip.crit.twist.p2p.NetworkMessage
import rip.crit.twist.p2p.PeerId
import rip.crit.twist.p2p.PeerNetwork
import rip.crit.twist.wire.FrameCodec
/** Basic TCP peer network with framed messages and an explicit adapter boundary. */
@SmartDoc(
summary = "TCP peer listener, handshake, and broadcast transport.",
category = "Networking",
)
class TcpPeerNetwork(
private val localId: PeerId,
private val requestedPort: Int = 0,
private val adapter: NetworkAdapter = EnvelopeAdapter(NetworkAdapterKind.DEVP2P),
) : PeerNetwork {
private val running = AtomicBoolean(false)
private val connections = ConcurrentHashMap<PeerId, Connection>()
private val listeners = CopyOnWriteArrayList<(NetworkMessage) -> Unit>()
private val workers = Executors.newCachedThreadPool()
private var server: ServerSocket? = null
val port: Int
get() = server?.localPort ?: -1
override fun start() {
if (!running.compareAndSet(false, true)) return
server = ServerSocket(requestedPort)
workers.execute {
while (running.get()) {
runCatching { server?.accept() }.getOrNull()?.let(::attach)
}
}
}
fun connect(host: String, port: Int, timeoutMillis: Int = 5_000) {
check(running.get()) { "Network is not running" }
val socket = Socket()
socket.connect(InetSocketAddress(host, port), timeoutMillis)
attach(socket)
}
fun subscribe(listener: (NetworkMessage) -> Unit) {
listeners += listener
}
override fun stop() {
if (!running.compareAndSet(true, false)) return
runCatching { server?.close() }
connections.values.forEach { it.close() }
connections.clear()
workers.shutdownNow()
}
override fun peers(): Set<PeerId> = connections.keys.toSet()
override fun broadcast(message: NetworkMessage) {
check(running.get()) { "Network is not running" }
connections.values.forEach { connection -> runCatching { connection.send(message) } }
}
private fun attach(socket: Socket) {
workers.execute {
val input = DataInputStream(socket.getInputStream())
val output = DataOutputStream(socket.getOutputStream())
output.writeUTF(localId.value)
output.flush()
val remote = PeerId(input.readUTF())
require(remote != localId) { "Self connection is not allowed" }
val connection = Connection(socket, input, output)
val previous = connections.put(remote, connection)
previous?.close()
try {
while (running.get() && !socket.isClosed) {
val size = input.readInt()
require(size in 0..FrameCodec.MAX_FRAME_SIZE) { "Invalid network frame length" }
val frame = ByteArray(size).also(input::readFully)
val message = adapter.decode(frame)
listeners.forEach { listener -> listener(message) }
}
} finally {
connections.remove(remote, connection)
connection.close()
}
}
}
private inner class Connection(
private val socket: Socket,
private val input: DataInputStream,
private val output: DataOutputStream,
) {
@Synchronized
fun send(message: NetworkMessage) {
val frame = adapter.encode(message)
require(frame.size <= FrameCodec.MAX_FRAME_SIZE)
output.writeInt(frame.size)
output.write(frame)
output.flush()
}
fun close() {
runCatching { input.close() }
runCatching { output.close() }
runCatching { socket.close() }
}
}
}

View file

@ -0,0 +1,36 @@
package rip.crit.twist.transport
import kotlin.random.Random
import rip.crit.twist.core.TwistSpecific
import rip.crit.twist.p2p.EnvelopeAdapter
import rip.crit.twist.p2p.NetworkAdapterKind
import rip.crit.twist.p2p.NetworkMessage
/** Twist overlay protected by the bundled AES-GCM implementation. */
class TwistOverlayAdapter(
@Suppress("UNUSED_PARAMETER") twistSpecific: TwistSpecific.Enabled,
private val key: ByteArray,
private val random: Random = Random.Default,
) : EnvelopeAdapter(NetworkAdapterKind.TWIST_OVERLAY) {
init {
require(key.size == 32)
}
override fun encode(message: NetworkMessage): ByteArray {
val nonce = random.nextBytes(12)
val encrypted = AesGcm(key).encrypt(nonce, super.encode(message))
return nonce + encrypted.ciphertext + encrypted.tag
}
override fun decode(frame: ByteArray): NetworkMessage {
require(frame.size > 28)
val plain =
AesGcm(key)
.decrypt(
frame.copyOfRange(0, 12),
frame.copyOfRange(12, frame.size - 16),
frame.copyOfRange(frame.size - 16, frame.size),
)
return super.decode(plain)
}
}

View file

@ -0,0 +1,84 @@
package rip.crit.twist.transport
import java.math.BigInteger
import rip.crit.twist.core.SmartDoc
/** RFC 7748 X25519 implementation using readable BigInteger field arithmetic. */
@SmartDoc(summary = "RFC 7748 X25519 key agreement primitives.", category = "Cryptography")
object X25519 {
private val prime = BigInteger.ONE.shiftLeft(255) - BigInteger.valueOf(19)
private val a24 = BigInteger.valueOf(121665)
private fun mod(value: BigInteger): BigInteger = value.mod(prime)
fun publicKey(privateKey: ByteArray): ByteArray =
scalarMult(privateKey, byteArrayOf(9) + ByteArray(31))
fun sharedSecret(privateKey: ByteArray, publicKey: ByteArray): ByteArray =
scalarMult(privateKey, publicKey).also {
require(it.any { byte -> byte.toInt() != 0 }) {
"X25519 rejected an all-zero shared secret"
}
}
fun scalarMult(privateKey: ByteArray, uCoordinate: ByteArray): ByteArray {
require(privateKey.size == 32 && uCoordinate.size == 32)
val scalarBytes =
privateKey.copyOf().also {
it[0] = (it[0].toInt() and 248).toByte()
it[31] = ((it[31].toInt() and 127) or 64).toByte()
}
val uBytes = uCoordinate.copyOf().also { it[31] = (it[31].toInt() and 127).toByte() }
val scalar = littleEndian(scalarBytes)
val x1 = littleEndian(uBytes).mod(prime)
var x2 = BigInteger.ONE
var z2 = BigInteger.ZERO
var x3 = x1
var z3 = BigInteger.ONE
var swap = 0
for (position in 254 downTo 0) {
val bit = if (scalar.testBit(position)) 1 else 0
swap = swap xor bit
if (swap != 0) {
val tx = x2
x2 = x3
x3 = tx
val tz = z2
z2 = z3
z3 = tz
}
swap = bit
val a = mod(x2 + z2)
val aa = mod(a * a)
val b = mod(x2 - z2)
val bb = mod(b * b)
val e = mod(aa - bb)
val c = mod(x3 + z3)
val d = mod(x3 - z3)
val da = mod(d * a)
val cb = mod(c * b)
x3 = mod((da + cb).pow(2))
z3 = mod(x1 * mod((da - cb).pow(2)))
x2 = mod(aa * bb)
z2 = mod(e * mod(aa + a24 * e))
}
if (swap != 0) {
val tx = x2
x2 = x3
x3 = tx
val tz = z2
z2 = z3
z3 = tz
}
return encodeLittleEndian(mod(x2 * z2.modInverse(prime)))
}
private fun littleEndian(bytes: ByteArray): BigInteger = BigInteger(1, bytes.reversedArray())
private fun encodeLittleEndian(value: BigInteger): ByteArray {
val source = value.toByteArray().dropWhile { it == 0.toByte() }.reversed()
return ByteArray(32).also { output ->
source.take(32).forEachIndexed { index, byte -> output[index] = byte }
}
}
}

View file

@ -0,0 +1,249 @@
package rip.crit.twist.tvm
import java.math.BigInteger
import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.bytecode.TwistBytecode
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Sha256
import rip.crit.twist.core.SmartDoc
import rip.crit.twist.core.Transaction
import rip.crit.twist.gas.BasicGasMeter
import rip.crit.twist.gas.GasLimit
import rip.crit.twist.state.ContractState
import rip.crit.twist.state.InMemoryWorldState
import rip.crit.twist.state.WorldState
/** Minimal deterministic stack VM for the Phase 3 instruction set. */
@SmartDoc(
summary = "Deterministic 256-bit stack virtual machine with metered execution.",
category = "Execution",
)
class BytecodeVirtualMachine(private val gasLimit: GasLimit = GasLimit(1_000_000)) :
VirtualMachine {
override fun execute(transaction: Transaction, state: WorldState): ExecutionResult {
if (state !is ContractState)
return ExecutionResult(
false,
rip.crit.twist.gas.GasUsed(0),
error = "ContractState is required",
)
val context =
ExecutionContext(
transaction.recipient ?: transaction.sender,
transaction.sender,
transaction.value,
)
return execute(transaction.payload, byteArrayOf(), StateExecutionHost(context, state))
}
fun execute(code: ByteArray, input: ByteArray, contract: Address): ExecutionResult =
execute(
code,
input,
StateExecutionHost(
ExecutionContext(contract, contract, Amount(BigInteger.ZERO)),
InMemoryWorldState(),
),
)
fun execute(code: ByteArray, input: ByteArray, host: ExecutionHost): ExecutionResult {
val meter = BasicGasMeter(gasLimit)
return try {
val instructions = TwistBytecode.decode(code)
val stack = ArrayDeque<Word256>()
val calldata = Calldata.wrap(input)
val transient = TransientStorage()
val memory = LinearMemory()
var pc = 0
while (pc in instructions.indices) {
val instruction = instructions[pc]
meter.consume(1)
when (instruction.opCode) {
OpCode.STOP ->
return ExecutionResult(
true,
meter.used,
stack.lastOrNull()?.toBytes() ?: byteArrayOf(),
)
OpCode.PUSH32 -> stack.addLast(Word256.fromBytes(instruction.immediate))
OpCode.ADD -> stack.addLast(stack.removeLast() + stack.removeLast())
OpCode.SUB -> {
val right = stack.removeLast()
stack.addLast(stack.removeLast() - right)
}
OpCode.MUL -> stack.addLast(stack.removeLast() * stack.removeLast())
OpCode.DIV -> {
val right = stack.removeLast()
stack.addLast(stack.removeLast() / right)
}
OpCode.EQ ->
stack.addLast(booleanWord(stack.removeLast() == stack.removeLast()))
OpCode.LT -> {
val right = stack.removeLast()
stack.addLast(booleanWord(stack.removeLast() < right))
}
OpCode.GT -> {
val right = stack.removeLast()
stack.addLast(booleanWord(stack.removeLast() > right))
}
OpCode.AND -> stack.addLast(stack.removeLast() and stack.removeLast())
OpCode.OR -> stack.addLast(stack.removeLast() or stack.removeLast())
OpCode.NOT -> stack.addLast(stack.removeLast().inv())
OpCode.DUP -> stack.addLast(stack.last())
OpCode.SWAP -> {
val a = stack.removeLast()
val b = stack.removeLast()
stack.addLast(a)
stack.addLast(b)
}
OpCode.CALLDATA_LOAD -> {
val offset = stack.removeLast().toBigInteger().intValueExact()
stack.addLast(calldata.wordAt(offset))
}
OpCode.TLOAD ->
stack.addLast(transient.get(host.context.contract, stack.removeLast()))
OpCode.TSTORE -> {
val value = stack.removeLast()
transient.put(host.context.contract, stack.removeLast(), value)
}
OpCode.SLOAD -> stack.addLast(host.load(stack.removeLast()))
OpCode.SSTORE -> {
val value = stack.removeLast()
host.store(stack.removeLast(), value)
}
OpCode.CALLER ->
stack.addLast(
Word256.fromBytes(
Sha256.bytes(host.context.caller.value.encodeToByteArray())
)
)
OpCode.CALLVALUE -> stack.addLast(Word256.of(host.context.value.value))
OpCode.TIMESTAMP ->
stack.addLast(Word256.fromLong(host.context.timestampMillis))
OpCode.BLOCK_NUMBER -> stack.addLast(Word256.fromLong(host.context.blockNumber))
OpCode.BALANCE ->
stack.addLast(
Word256.of(
host.balance(Address(stack.removeLast().toBytes().hex())).value
)
)
OpCode.SHA256 ->
stack.addLast(Word256.fromBytes(Sha256.bytes(stack.removeLast().toBytes())))
OpCode.MOD -> {
val right = stack.removeLast()
stack.addLast(stack.removeLast() % right)
}
OpCode.XOR -> stack.addLast(stack.removeLast() xor stack.removeLast())
OpCode.ISZERO -> stack.addLast(booleanWord(stack.removeLast() == Word256.ZERO))
OpCode.POP -> stack.removeLast()
OpCode.SHL -> {
val bits = stack.removeLast().toBigInteger().intValueExact()
stack.addLast(stack.removeLast().shiftLeft(bits))
}
OpCode.SHR -> {
val bits = stack.removeLast().toBigInteger().intValueExact()
stack.addLast(stack.removeLast().shiftRight(bits))
}
OpCode.BYTE -> {
val index = stack.removeLast().toBigInteger().intValueExact()
stack.addLast(stack.removeLast().byte(index))
}
OpCode.MLOAD ->
stack.addLast(
Word256.fromBytes(
memory.load(stack.removeLast().toBigInteger().intValueExact(), 32)
)
)
OpCode.MSTORE -> {
val value = stack.removeLast()
val offset = stack.removeLast().toBigInteger().intValueExact()
memory.store(offset, value.toBytes())
}
OpCode.MSIZE -> stack.addLast(Word256.fromLong(memory.size.toLong()))
OpCode.CALLDATA_SIZE -> stack.addLast(Word256.fromLong(calldata.size.toLong()))
OpCode.CODE_SIZE -> stack.addLast(Word256.fromLong(code.size.toLong()))
OpCode.GAS -> stack.addLast(Word256.fromLong(gasLimit.value - meter.used.value))
OpCode.ADDRESS -> stack.addLast(addressWord(host.context.contract))
OpCode.ORIGIN -> stack.addLast(addressWord(host.context.origin))
OpCode.CHAIN_ID -> stack.addLast(Word256.fromLong(host.context.chainId))
OpCode.LOG -> {
val data = stack.removeLast()
host.log(stack.removeLast(), data)
}
OpCode.CALL -> {
val address = Address(stack.removeLast().toBytes().hex())
stack.addLast(
Word256.fromBytes(Sha256.bytes(host.call(address, byteArrayOf())))
)
}
OpCode.JUMP -> {
pc = stack.removeLast().toBigInteger().intValueExact()
require(pc in instructions.indices)
continue
}
OpCode.JUMPI -> {
val target = stack.removeLast().toBigInteger().intValueExact()
if (stack.removeLast() != Word256.ZERO) {
pc = target
require(pc in instructions.indices)
continue
}
}
OpCode.RETURN ->
return ExecutionResult(
true,
meter.used,
stack.lastOrNull()?.toBytes() ?: byteArrayOf(),
)
OpCode.REVERT ->
return ExecutionResult(false, meter.used, error = "Execution reverted")
OpCode.CREATE -> {
val value = Amount(stack.removeLast().toBigInteger())
val created = host.create(stack.removeLast().toBytes(), value)
stack.addLast(addressWord(created))
}
OpCode.SELFDESTRUCT -> {
host.selfDestruct(Address(stack.removeLast().toBytes().hex()))
return ExecutionResult(true, meter.used)
}
OpCode.INVALID -> error("Invalid opcode")
}
pc++
}
ExecutionResult(true, meter.used, stack.lastOrNull()?.toBytes() ?: byteArrayOf())
} catch (error: Exception) {
ExecutionResult(false, meter.used, error = error.message)
}
}
private fun booleanWord(value: Boolean): Word256 =
if (value) Word256.fromLong(1) else Word256.ZERO
private fun addressWord(address: Address): Word256 =
Word256.fromBytes(Sha256.bytes(address.value.encodeToByteArray()))
private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) }
}
/** Expand-on-write memory with a fixed safety ceiling. */
private class LinearMemory(private val maximumSize: Int = 16 * 1024 * 1024) {
private var bytes = ByteArray(0)
val size: Int
get() = bytes.size
fun load(offset: Int, length: Int): ByteArray {
require(offset >= 0 && length >= 0 && offset <= maximumSize - length)
val output = ByteArray(length)
if (offset < bytes.size)
bytes.copyInto(output, 0, offset, minOf(bytes.size, offset + length))
return output
}
fun store(offset: Int, value: ByteArray) {
require(offset >= 0 && offset <= maximumSize - value.size)
val required = offset + value.size
if (required > bytes.size) bytes = bytes.copyOf(required)
value.copyInto(bytes, offset)
}
}

View file

@ -0,0 +1,119 @@
package rip.crit.twist.tvm
import java.math.BigInteger
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
import rip.crit.twist.state.ContractState
data class ExecutionContext(
val contract: Address,
val caller: Address,
val value: Amount,
val blockNumber: Long = 0,
val timestampMillis: Long = 0,
val origin: Address = caller,
val chainId: Long = 1,
val callDepth: Int = 0,
)
data class ContractLog(val contract: Address, val topic: Word256, val data: Word256)
interface ExecutionHost {
val context: ExecutionContext
fun load(key: Word256): Word256
fun store(key: Word256, value: Word256)
fun balance(address: Address): Amount
fun log(topic: Word256, data: Word256)
fun call(address: Address, input: ByteArray): ByteArray
fun create(code: ByteArray, value: Amount): Address
fun selfDestruct(beneficiary: Address)
}
class StateExecutionHost(override val context: ExecutionContext, private val state: ContractState) :
ExecutionHost {
val logs = mutableListOf<ContractLog>()
override fun load(key: Word256): Word256 =
state.storage(context.contract, Hash(key.toBytes().hex()))?.let(Word256::fromBytes)
?: Word256.ZERO
override fun store(key: Word256, value: Word256) =
state.putStorage(context.contract, Hash(key.toBytes().hex()), value.toBytes())
override fun balance(address: Address): Amount =
state.account(address)?.balance ?: Amount(BigInteger.ZERO)
override fun log(topic: Word256, data: Word256) {
logs += ContractLog(context.contract, topic, data)
}
override fun call(address: Address, input: ByteArray): ByteArray {
require(context.callDepth < MAX_CALL_DEPTH) { "Maximum call depth exceeded" }
val code = state.account(address)?.code ?: return byteArrayOf()
val child =
StateExecutionHost(
context.copy(
contract = address,
caller = context.contract,
value = Amount(BigInteger.ZERO),
callDepth = context.callDepth + 1,
),
state,
)
val result = BytecodeVirtualMachine().execute(code, input, child)
require(result.succeeded) { result.error ?: "Nested call failed" }
logs += child.logs
return result.returnData
}
override fun create(code: ByteArray, value: Amount): Address {
val creator = state.account(context.contract)
require(value.value <= (creator?.balance?.value ?: BigInteger.ZERO)) {
"Insufficient creation balance"
}
val seed =
context.contract.value.encodeToByteArray() +
(creator?.nonce ?: 0).toString().encodeToByteArray() +
code
val address = Address(Sha256.digest(seed).value.takeLast(40))
require(state.account(address) == null) { "Contract address collision" }
if (creator != null) {
state.putAccount(
context.contract,
creator.copy(
nonce = creator.nonce + 1,
balance = Amount(creator.balance.value - value.value),
),
)
}
state.putAccount(address, rip.crit.twist.state.Account(balance = value, code = code))
return address
}
override fun selfDestruct(beneficiary: Address) {
val account = state.account(context.contract) ?: return
val recipient =
state.account(beneficiary)
?: rip.crit.twist.state.Account(balance = Amount(BigInteger.ZERO))
state.putAccount(
beneficiary,
recipient.copy(balance = Amount(recipient.balance.value + account.balance.value)),
)
state.deleteAccount(context.contract)
}
private fun ByteArray.hex() = joinToString("") { "%02x".format(it) }
private companion object {
const val MAX_CALL_DEPTH = 64
}
}

View file

@ -0,0 +1,16 @@
package rip.crit.twist.tvm
import java.util.concurrent.ConcurrentHashMap
import rip.crit.twist.core.Address
class TransientStorage {
private val values = ConcurrentHashMap<Address, ConcurrentHashMap<Word256, Word256>>()
fun get(contract: Address, key: Word256): Word256 = values[contract]?.get(key) ?: Word256.ZERO
fun put(contract: Address, key: Word256, value: Word256) {
values.computeIfAbsent(contract) { ConcurrentHashMap() }[key] = value
}
fun clear() = values.clear()
}

View file

@ -0,0 +1,38 @@
package rip.crit.twist.tvm
import rip.crit.twist.core.Transaction
import rip.crit.twist.gas.GasUsed
import rip.crit.twist.state.WorldState
data class ExecutionResult(
val succeeded: Boolean,
val gasUsed: GasUsed,
val returnData: ByteArray = byteArrayOf(),
val error: String? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ExecutionResult
if (succeeded != other.succeeded) return false
if (gasUsed != other.gasUsed) return false
if (!returnData.contentEquals(other.returnData)) return false
if (error != other.error) return false
return true
}
override fun hashCode(): Int {
var result = succeeded.hashCode()
result = 31 * result + gasUsed.hashCode()
result = 31 * result + returnData.contentHashCode()
result = 31 * result + error.hashCode()
return result
}
}
interface VirtualMachine {
fun execute(transaction: Transaction, state: WorldState): ExecutionResult
}

View file

@ -0,0 +1,88 @@
package rip.crit.twist.tvm
import java.math.BigInteger
import java.nio.ByteBuffer
/** Unsigned EVM-style integer with modulo 2^256 arithmetic. */
@JvmInline
value class Word256 private constructor(private val raw: BigInteger) : Comparable<Word256> {
operator fun plus(other: Word256): Word256 = of(raw + other.raw)
operator fun minus(other: Word256): Word256 = of(raw - other.raw)
operator fun times(other: Word256): Word256 = of(raw * other.raw)
operator fun div(other: Word256): Word256 = if (other == ZERO) ZERO else of(raw / other.raw)
operator fun rem(other: Word256): Word256 = if (other == ZERO) ZERO else of(raw % other.raw)
infix fun and(other: Word256): Word256 = of(raw.and(other.raw))
infix fun or(other: Word256): Word256 = of(raw.or(other.raw))
infix fun xor(other: Word256): Word256 = of(raw.xor(other.raw))
fun inv(): Word256 = of(raw.xor(MODULUS - BigInteger.ONE))
fun shiftLeft(bits: Int): Word256 = if (bits >= 256) ZERO else of(raw.shiftLeft(bits))
fun shiftRight(bits: Int): Word256 = if (bits >= 256) ZERO else of(raw.shiftRight(bits))
fun byte(index: Int): Word256 =
if (index !in 0 until BYTE_SIZE) ZERO else fromLong(toBytes()[index].toLong() and 0xff)
fun toBigInteger(): BigInteger = raw
fun toBytes(): ByteArray =
raw.toByteArray().let { source ->
ByteArray(BYTE_SIZE).also { target ->
source.copyInto(
target,
BYTE_SIZE - source.size.coerceAtMost(BYTE_SIZE),
(source.size - BYTE_SIZE).coerceAtLeast(0),
)
}
}
override fun compareTo(other: Word256): Int = raw.compareTo(other.raw)
companion object {
const val BYTE_SIZE = 32
private val MODULUS = BigInteger.ONE.shiftLeft(256)
val ZERO = Word256(BigInteger.ZERO)
fun of(value: BigInteger): Word256 = Word256(value.mod(MODULUS))
fun fromBytes(bytes: ByteArray): Word256 {
require(bytes.size <= BYTE_SIZE)
return of(BigInteger(1, bytes))
}
fun fromLong(value: Long): Word256 {
require(value >= 0)
return of(BigInteger.valueOf(value))
}
}
}
/** Read-only calldata view. Its slices share the original byte buffer. */
class Calldata private constructor(private val buffer: ByteBuffer) {
val size: Int
get() = buffer.capacity()
fun wordAt(offset: Int): Word256 {
require(offset >= 0 && offset + Word256.BYTE_SIZE <= size)
val bytes = ByteArray(Word256.BYTE_SIZE)
buffer.duplicate().position(offset).get(bytes)
return Word256.fromBytes(bytes)
}
fun slice(offset: Int, length: Int): ByteBuffer {
require(offset >= 0 && length >= 0 && offset + length <= size)
return buffer.duplicate().position(offset).limit(offset + length).slice().asReadOnlyBuffer()
}
companion object {
fun wrap(bytes: ByteArray): Calldata = Calldata(ByteBuffer.wrap(bytes).asReadOnlyBuffer())
}
}

View file

@ -0,0 +1,138 @@
package rip.crit.twist.twisto
import java.math.BigInteger
import rip.crit.twist.bips.StorageContract
import rip.crit.twist.compiler.AccessOperation
import rip.crit.twist.compiler.ContractIr
import rip.crit.twist.compiler.contract
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
import rip.crit.twist.core.SmartDoc
import rip.crit.twist.standards.PersistentToken
import rip.crit.twist.standards.TokenStandard
import rip.crit.twist.store.KeyValueStore
data class TwistoMetadata(
val schema: String = "twisto-metadata/1",
val name: String = "Twisto",
val symbol: String = "TWISTO",
val description: String,
val image: String? = null,
val externalUrl: String? = null,
val issuerDid: String? = null,
val attributes: Map<String, String> = emptyMap(),
) {
init {
require(schema == "twisto-metadata/1")
require(name.isNotBlank() && name.length <= 128)
require(symbol.matches(Regex("[A-Z0-9]{2,12}")))
require(description.length <= 4_096)
require(
attributes.size <= 64 &&
attributes.all { it.key.length <= 64 && it.value.length <= 512 }
)
}
/** Canonical JSON manifest, suitable for content addressing. */
fun encode(): ByteArray = buildString {
append("{\"schema\":\"")
append(schema.escape())
append("\",\"name\":\"")
append(name.escape())
append("\",\"symbol\":\"")
append(symbol.escape())
append("\",\"description\":\"")
append(description.escape())
append("\",\"image\":")
appendJson(image)
append(",\"externalUrl\":")
appendJson(externalUrl)
append(",\"issuerDid\":")
appendJson(issuerDid)
append(",\"attributes\":{")
append(
attributes.toSortedMap().entries.joinToString(",") {
"\"${it.key.escape()}\":\"${it.value.escape()}\""
}
)
append("}}")
}
.encodeToByteArray()
private fun String.escape(): String =
replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
private fun StringBuilder.appendJson(value: String?) {
if (value == null) append("null") else append("\"").append(value.escape()).append("\"")
}
}
/** Named token example whose immutable metadata is addressed through decentralized storage. */
@SmartDoc(summary = "Named token with immutable decentralized metadata.", category = "Contracts")
class TwistoToken(
private val owner: Address,
private val state: KeyValueStore,
private val content: StorageContract,
) : TokenStandard {
private val token = PersistentToken(SYMBOL, state)
override val symbol: String = SYMBOL
fun deploy(metadata: TwistoMetadata, initialSupply: Amount): Hash =
synchronized(this) {
check(state.get(DEPLOYED) == null) { "Twisto is already deployed" }
require(metadata.symbol == SYMBOL)
val pointer = content.put(metadata.encode())
state.put(METADATA, pointer.value.encodeToByteArray())
state.put(SUPPLY, initialSupply.value.toByteArray())
state.put(DEPLOYED, byteArrayOf(1))
if (initialSupply.value.signum() > 0) token.mint(owner, initialSupply)
pointer
}
fun metadataPointer(): Hash? = state.get(METADATA)?.decodeToString()?.let(::Hash)
fun metadata(): ByteArray? = metadataPointer()?.let(content::get)
fun totalSupply(): Amount = Amount(state.get(SUPPLY)?.let(::BigInteger) ?: BigInteger.ZERO)
fun mint(caller: Address, recipient: Address, amount: Amount) =
synchronized(this) {
require(caller == owner) { "Only the owner may mint" }
check(state.get(DEPLOYED) != null) { "Twisto is not deployed" }
require(amount.value.signum() > 0)
token.mint(recipient, amount)
state.put(SUPPLY, (totalSupply().value + amount.value).toByteArray())
}
override fun balanceOf(owner: Address): Amount = token.balanceOf(owner)
override fun transfer(from: Address, to: Address, amount: Amount): Boolean =
token.transfer(from, to, amount)
fun contractIr(): ContractIr =
contract("Twisto") {
allow(AccessOperation.READ, "storage:*", "*")
allow(AccessOperation.WRITE, "storage:*", owner.value)
allow(AccessOperation.EXECUTE, "*", "*")
function("balanceOf", 1) {
storageLoad()
returnWord()
}
function("transfer", 2) {
storageStore()
returnWord()
}
function("metadata", 3) {
storageLoad()
returnWord()
}
}
private companion object {
const val SYMBOL = "TWISTO"
val DEPLOYED = "twisto:deployed".encodeToByteArray()
val METADATA = "twisto:metadata".encodeToByteArray()
val SUPPLY = "twisto:supply".encodeToByteArray()
}
}

View file

@ -0,0 +1,24 @@
package rip.crit.twist.wire
import java.nio.ByteBuffer
/** Stream for wire payloads. */
object FrameCodec {
const val MAX_FRAME_SIZE = 16 * 1024 * 1024
fun encode(payload: ByteArray): ByteArray {
require(payload.size <= MAX_FRAME_SIZE) { "Frame exceeds maximum size" }
return ByteBuffer.allocate(Int.SIZE_BYTES + payload.size)
.putInt(payload.size)
.put(payload)
.array()
}
fun decode(frame: ByteArray): ByteArray {
require(frame.size >= Int.SIZE_BYTES) { "Truncated frame" }
val buffer = ByteBuffer.wrap(frame)
val size = buffer.int
require(size in 0..MAX_FRAME_SIZE && size == buffer.remaining()) { "Invalid frame length" }
return ByteArray(size).also(buffer::get)
}
}

View file

@ -0,0 +1,73 @@
package rip.crit.twist.wire
import java.nio.ByteBuffer
import rip.crit.twist.core.Sha256
/** Twist wire v1 packet, big-endian and checksum-protected. */
data class WirePacket(val type: PacketType, val payload: ByteArray, val flags: Int = 0) {
override fun equals(other: Any?): Boolean =
other is WirePacket &&
type == other.type &&
flags == other.flags &&
payload.contentEquals(other.payload)
override fun hashCode(): Int = 31 * (31 * type.hashCode() + flags) + payload.contentHashCode()
}
enum class PacketType(val id: Byte) {
HELLO(1),
PING(2),
PONG(3),
GOSSIP(4),
DHT_LOOKUP(5),
DHT_RECORD(6),
TRANSACTION(7),
BLOCK(8),
}
object PacketCodec {
private const val MAGIC = 0x54575354
private const val HEADER_SIZE = 4 + 1 + 1 + 2 + 4 + 32
const val MAX_PAYLOAD_SIZE = 16 * 1024 * 1024
fun encode(packet: WirePacket): ByteArray {
require(packet.flags in 0..0xffff && packet.payload.size <= MAX_PAYLOAD_SIZE)
val checksum =
Sha256.digest(packet.payload)
.value
.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
return ByteBuffer.allocate(HEADER_SIZE + packet.payload.size)
.putInt(MAGIC)
.put(1)
.put(packet.type.id)
.putShort(packet.flags.toShort())
.putInt(packet.payload.size)
.put(checksum)
.put(packet.payload)
.array()
}
fun decode(bytes: ByteArray): WirePacket {
require(bytes.size >= HEADER_SIZE)
val input = ByteBuffer.wrap(bytes)
require(input.int == MAGIC && input.get().toInt() == 1)
val typeId = input.get()
val type =
PacketType.entries.firstOrNull { it.id == typeId } ?: error("Unknown packet type")
val flags = input.short.toInt() and 0xffff
val size = input.int
require(size in 0..MAX_PAYLOAD_SIZE && input.remaining() == 32 + size)
val checksum = ByteArray(32).also(input::get)
val payload = ByteArray(size).also(input::get)
require(
checksum.contentEquals(
Sha256.digest(payload).value.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
)
) {
"Packet checksum mismatch"
}
return WirePacket(type, payload, flags)
}
}

View file

@ -0,0 +1,145 @@
package rip.crit.twist.wire
import java.io.ByteArrayOutputStream
import java.math.BigInteger
/**
* Ethereum Recursive Length Prefix (RLP) codec.
*
* Wire reference: ethereum/devp2p `rlpx.md` — RLPx framing, auth/ack handshake
* payloads, and devp2p Hello/Disconnect/Ping/Pong bodies are all RLP-encoded.
* Single-byte values < 0x80 are their own encoding; all other strings/lists use
* short (<=55 bytes) and long length prefixes.
*/
object Rlp {
private const val SHORT_MAX = 55
fun encodeString(bytes: ByteArray): ByteArray =
when {
bytes.size == 1 && (bytes[0].toInt() and 0xFF) < 0x80 -> bytes.copyOf()
bytes.size <= SHORT_MAX ->
byteArrayOf((0x80 + bytes.size).toByte()) + bytes
else -> {
val len = lengthBytes(bytes.size)
byteArrayOf((0xB7 + len.size).toByte()) + len + bytes
}
}
fun encodeInt(value: BigInteger): ByteArray {
require(value >= BigInteger.ZERO) { "RLP integers must be non-negative" }
if (value == BigInteger.ZERO) return byteArrayOf(0x80.toByte())
var raw = value.toByteArray().dropWhile { it == 0.toByte() }.toByteArray()
return encodeString(raw)
}
fun encodeInt(value: Long): ByteArray = encodeInt(BigInteger.valueOf(value))
fun encodeList(items: List<ByteArray>): ByteArray {
val payload = items.fold(ByteArray(0)) { acc, item -> acc + item }
return when {
payload.size <= SHORT_MAX ->
byteArrayOf((0xC0 + payload.size).toByte()) + payload
else -> {
val len = lengthBytes(payload.size)
byteArrayOf((0xF7 + len.size).toByte()) + len + payload
}
}
}
fun encodeElements(vararg items: ByteArray): ByteArray = encodeList(items.toList())
/** Decodes one RLP item starting at [offset]; returns (item, nextOffset). */
fun decodeOne(bytes: ByteArray, offset: Int = 0): Pair<RlpItem, Int> {
require(offset < bytes.size) { "RLP truncated" }
val prefix = bytes[offset].toInt() and 0xFF
return when {
prefix < 0x80 -> Pair(RlpItem.String(bytes.copyOfRange(offset, offset + 1)), offset + 1)
prefix <= 0xB7 -> {
val len = prefix - 0x80
require(offset + 1 + len <= bytes.size) { "RLP string truncated" }
Pair(RlpItem.String(bytes.copyOfRange(offset + 1, offset + 1 + len)), offset + 1 + len)
}
prefix <= 0xBF -> {
val lenOfLen = prefix - 0xB7
val len = readLength(bytes, offset + 1, lenOfLen)
require(offset + 1 + lenOfLen + len <= bytes.size) { "RLP long string truncated" }
Pair(
RlpItem.String(bytes.copyOfRange(offset + 1 + lenOfLen, offset + 1 + lenOfLen + len)),
offset + 1 + lenOfLen + len)
}
prefix <= 0xF7 -> {
val len = prefix - 0xC0
require(offset + 1 + len <= bytes.size) { "RLP list truncated" }
Pair(decodeList(bytes.copyOfRange(offset + 1, offset + 1 + len)), offset + 1 + len)
}
else -> {
val lenOfLen = prefix - 0xF7
val len = readLength(bytes, offset + 1, lenOfLen)
require(offset + 1 + lenOfLen + len <= bytes.size) { "RLP long list truncated" }
Pair(
decodeList(bytes.copyOfRange(offset + 1 + lenOfLen, offset + 1 + lenOfLen + len)),
offset + 1 + lenOfLen + len)
}
}
}
fun decode(bytes: ByteArray): RlpItem {
val (item, next) = decodeOne(bytes, 0)
require(next == bytes.size) { "Trailing RLP bytes" }
return item
}
private fun decodeList(payload: ByteArray): RlpItem.List {
val items = mutableListOf<RlpItem>()
var offset = 0
while (offset < payload.size) {
val (item, next) = decodeOne(payload, offset)
items += item
offset = next
}
require(offset == payload.size) { "RLP list overrun" }
return RlpItem.List(items)
}
private fun readLength(bytes: ByteArray, offset: Int, lenOfLen: Int): Int {
require(lenOfLen in 1..4 && offset + lenOfLen <= bytes.size) { "Bad RLP length prefix" }
require(bytes[offset] != 0.toByte()) { "RLP length has leading zero" }
var len = 0
for (i in 0 until lenOfLen) len = (len shl 8) or (bytes[offset + i].toInt() and 0xFF)
require(len > SHORT_MAX) { "RLP long form used for short payload" }
return len
}
private fun lengthBytes(size: Int): ByteArray {
val out = ByteArrayOutputStream()
var v = size
val tmp = mutableListOf<Byte>()
while (v > 0) {
tmp += (v and 0xFF).toByte()
v = v ushr 8
}
tmp.reversed().forEach { out.write(it.toInt()) }
return out.toByteArray()
}
}
sealed interface RlpItem {
data class String(val bytes: ByteArray) : RlpItem {
fun asLong(): Long {
require(bytes.isNotEmpty()) { "Empty RLP int" }
require(!(bytes.size > 1 && bytes[0] == 0.toByte())) { "Non-canonical RLP int" }
var v = 0L
for (b in bytes) v = (v shl 8) or (b.toLong() and 0xFF)
return v
}
fun asText(): kotlin.String = bytes.decodeToString()
override fun equals(other: Any?): Boolean =
other is String && bytes.contentEquals(other.bytes)
override fun hashCode(): Int = bytes.contentHashCode()
}
data class List(val items: kotlin.collections.List<RlpItem>) : RlpItem
}

View file

@ -0,0 +1,92 @@
package rip.crit.twist.wire
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.math.BigInteger
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Transaction
import rip.crit.twist.p2p.NetworkMessage
/** Length-prefixed envelope shared by devp2p-twist subprotocol payloads. */
object WireEnvelope {
fun encode(message: NetworkMessage): ByteArray = WireCodec.encode(message)
fun decode(bytes: ByteArray): NetworkMessage = WireCodec.decodeNetworkMessage(bytes)
}
object WireCodec {
private const val VERSION: Byte = 1
private const val MAX_FIELD_SIZE = 16 * 1024 * 1024
fun encode(transaction: Transaction): ByteArray = bytes {
writeByte(VERSION.toInt())
writeString(transaction.id.value)
writeString(transaction.sender.value)
writeBoolean(transaction.recipient != null)
transaction.recipient?.let { writeString(it.value) }
writeLong(transaction.nonce)
writeBytes(transaction.value.value.toByteArray())
writeBytes(transaction.payload)
}
fun decodeTransaction(bytes: ByteArray): Transaction =
DataInputStream(ByteArrayInputStream(bytes)).use {
requireVersion(it)
val id = Hash(it.readString())
val sender = Address(it.readString())
val recipient = if (it.readBoolean()) Address(it.readString()) else null
val nonce = it.readLong()
require(nonce >= 0) { "Transaction nonce cannot be negative" }
val value = Amount(BigInteger(it.readBytes()))
val payload = it.readBytes()
require(it.available() == 0) { "Trailing transaction bytes" }
Transaction(id, sender, recipient, nonce, value, payload)
}
fun encode(message: NetworkMessage): ByteArray = bytes {
writeByte(VERSION.toInt())
writeString(message.id.value)
writeString(message.topic)
writeBytes(message.payload)
}
fun decodeNetworkMessage(bytes: ByteArray): NetworkMessage =
DataInputStream(ByteArrayInputStream(bytes)).use {
requireVersion(it)
val id = Hash(it.readString())
val topic = it.readString()
val message = NetworkMessage(topic, it.readBytes(), id)
require(it.available() == 0) { "Trailing message bytes" }
message
}
private fun bytes(write: DataOutputStream.() -> Unit): ByteArray =
ByteArrayOutputStream().use { output ->
DataOutputStream(output).use { it.write() }
output.toByteArray()
}
private fun requireVersion(input: DataInputStream) {
require(input.readByte() == VERSION) { "Unsupported wire version" }
}
private fun DataOutputStream.writeString(value: String) = writeBytes(value.encodeToByteArray())
private fun DataOutputStream.writeBytes(value: ByteArray) {
require(value.size <= MAX_FIELD_SIZE)
writeInt(value.size)
write(value)
}
private fun DataInputStream.readString(): String = readBytes().decodeToString()
private fun DataInputStream.readBytes(): ByteArray {
val size = readInt()
require(size in 0..MAX_FIELD_SIZE) { "Invalid wire field length" }
return ByteArray(size).also(::readFully)
}
}

View file

@ -0,0 +1,465 @@
package rip.crit.twist
import rip.crit.twist.access.AccessList
import rip.crit.twist.bips.FileStorageContract
import rip.crit.twist.bytecode.TwistBytecode
import rip.crit.twist.compiler.TwistCompiler
import rip.crit.twist.compiler.LispIrCompiler
import rip.crit.twist.compiler.contract
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
import rip.crit.twist.core.Sha256
import rip.crit.twist.core.TransactionHasher
import rip.crit.twist.core.TwistSpecific
import rip.crit.twist.gas.BasicGasMeter
import rip.crit.twist.gas.GasLimit
import rip.crit.twist.gas.OutOfGasException
import rip.crit.twist.merkle.Sha256MerkleTree
import rip.crit.twist.merkle.verifies
import rip.crit.twist.p2p.NetworkMessage
import rip.crit.twist.p2p.InMemoryDht
import rip.crit.twist.p2p.PeerId
import rip.crit.twist.proof.ProofOfAuthority
import rip.crit.twist.proof.ProofOfWork
import rip.crit.twist.state.Account
import rip.crit.twist.state.FileWorldState
import rip.crit.twist.state.InMemoryWorldState
import rip.crit.twist.store.InMemoryKeyValueStore
import rip.crit.twist.wire.FrameCodec
import rip.crit.twist.wire.WireCodec
import rip.crit.twist.wire.PacketCodec
import rip.crit.twist.wire.PacketType
import rip.crit.twist.wire.WirePacket
import rip.crit.twist.transport.X25519Session
import rip.crit.twist.transport.Aes
import rip.crit.twist.transport.AesGcm
import rip.crit.twist.transport.X25519
import rip.crit.twist.transport.TwistOverlayAdapter
import rip.crit.twist.ecdsa.Secp256k1Ecdsa
import rip.crit.twist.rsa.RsaKeyPair
import rip.crit.twist.rsa.RsaOaep
import rip.crit.twist.store.FileKeyValueStore
import rip.crit.twist.tvm.Calldata
import rip.crit.twist.tvm.TransientStorage
import rip.crit.twist.tvm.Word256
import rip.crit.twist.tvm.BytecodeVirtualMachine
import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.p2p.PeerNetwork
import rip.crit.twist.transport.TcpPeerNetwork
import rip.crit.twist.p2p.DevP2pAdapter
import rip.crit.twist.p2p.DevP2pHello
import rip.crit.twist.p2p.LibP2pAdapter
import rip.crit.twist.p2p.ProtocolCapability
import rip.crit.twist.router.NetworkResultDistributor
import rip.crit.twist.router.OffchainComputation
import rip.crit.twist.router.OffchainJob
import rip.crit.twist.router.OffchainRouter
import rip.crit.twist.router.OffchainWorker
import rip.crit.twist.router.ProofPolicy
import java.math.BigInteger
import java.nio.file.Files
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertFailsWith
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import rip.crit.twist.did.DidKey
import rip.crit.twist.did.DidMethodRegistry
import rip.crit.twist.did.DidTwist
import rip.crit.twist.did.KeyCodec
import rip.crit.twist.miner.CpuMiner
import rip.crit.twist.reflect.NetworkReflector
import rip.crit.twist.reflect.PeerIndex
import rip.crit.twist.reflect.PeerObservation
import rip.crit.twist.reflect.PeerProbe
import rip.crit.twist.twisto.TwistoMetadata
import rip.crit.twist.twisto.TwistoToken
import rip.crit.twist.smartdoc.SmartDocGenerator
import java.nio.file.Path
class AppTest {
@Test
fun accessListsIdentifyWriteConflicts() {
val alice = Address("alice")
val bob = Address("bob")
assertTrue(AccessList(writes = setOf(alice)).conflictsWith(AccessList(reads = setOf(alice))))
assertFalse(AccessList(writes = setOf(alice)).conflictsWith(AccessList(reads = setOf(bob))))
}
@Test
fun sha256AndTransactionIdsAreDeterministic() {
assertEquals(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Sha256.digest(byteArrayOf()).value
)
val transaction =
TransactionHasher.create(Address("alice"), Address("bob"), 1, Amount(BigInteger.TEN), byteArrayOf(1))
assertEquals(
transaction.id,
TransactionHasher.hash(
transaction.sender,
transaction.recipient,
transaction.nonce,
transaction.value,
transaction.payload
)
)
}
@Test
fun merkleProofVerifiesAndStateIsDefensive() {
val leaves = listOf("one", "two", "three").map(Sha256::digestUtf8)
val tree = Sha256MerkleTree(leaves)
assertTrue(tree.proofFor(leaves[1])!!.verifies(tree.root))
val state = InMemoryWorldState()
val account = Account(balance = Amount(BigInteger.ONE), code = byteArrayOf(7))
state.putAccount(Address("alice"), account)
account.code[0] = 9
assertEquals(7, state.account(Address("alice"))!!.code[0])
assertNotEquals(state.rootHash(), Sha256.digestUtf8("different"))
}
@Test
fun storesCopyValuesAndGasCannotOverflowLimit() {
val store = InMemoryKeyValueStore()
val value = byteArrayOf(1)
store.put(byteArrayOf(1), value)
value[0] = 2
assertEquals(1, store.get(byteArrayOf(1))!![0])
val meter = BasicGasMeter(GasLimit(3))
meter.consume(3)
assertEquals(3, meter.used.value)
assertFailsWith<OutOfGasException> { meter.consume(1) }
assertNotNull(store.get(byteArrayOf(1)))
}
@Test
fun wireCodecRoundTripsTransactionsAndNetworkFrames() {
val transaction =
TransactionHasher.create(Address("alice"), Address("bob"), 4, Amount(BigInteger.TEN), byteArrayOf(1, 2))
assertEquals(transaction, WireCodec.decodeTransaction(WireCodec.encode(transaction)))
assertEquals(byteArrayOf(1, 2).toList(), FrameCodec.decode(FrameCodec.encode(byteArrayOf(1, 2))).toList())
val message = NetworkMessage("transactions", byteArrayOf(9), Hash("message-id"))
assertEquals(message, WireCodec.decodeNetworkMessage(WireCodec.encode(message)))
}
@Test
fun tcpPeerNetworksHandshakeAndBroadcast() {
val first = TcpPeerNetwork(PeerId("first"))
val second = TcpPeerNetwork(PeerId("second"))
val received = CountDownLatch(1)
second.subscribe { received.countDown() }
first.start()
second.start()
try {
second.connect("127.0.0.1", first.port)
repeat(50) {
if (first.peers().contains(PeerId("second"))) return@repeat
Thread.sleep(10)
}
first.broadcast(NetworkMessage("test", byteArrayOf(1), Hash("tcp-message")))
assertTrue(received.await(2, TimeUnit.SECONDS))
assertTrue(first.peers().contains(PeerId("second")))
} finally {
first.stop()
second.stop()
}
}
@Test
fun compatibilityAdaptersNegotiateAndPreserveMessages() {
val dev = DevP2pAdapter(setOf(ProtocolCapability("twist", 1), ProtocolCapability("twist", 2)))
val hello = DevP2pHello("peer", 9000, PeerId("remote"), setOf(ProtocolCapability("twist", 2)))
assertEquals(setOf(ProtocolCapability("twist", 2)), dev.negotiate(hello))
val message = NetworkMessage("blocks", byteArrayOf(2), Hash("adapter-message"))
assertEquals(message, dev.decode(dev.encode(message)))
val lib = LibP2pAdapter(setOf("/twist/1.0.0", "/twist/2.0.0"))
assertEquals("/twist/2.0.0", lib.select(setOf("/twist/2.0.0")))
assertEquals(message, lib.decode(lib.encode(message)))
}
@Test
fun smartDocProducesBrowsableStaticReference() {
val output = Files.createTempDirectory("twist-smartdoc")
val entries = SmartDocGenerator.generate(Path.of("app/src/main/kotlin"), output)
assertTrue(entries.any { it.name == "BytecodeVirtualMachine" })
assertTrue(Files.readString(output.resolve("index.html")).contains("Twist SmartDoc API"))
}
@Test
fun phaseThreeCompilerAndStoragePrimitivesWork() {
assertEquals(Word256.ZERO, Word256.of(BigInteger.ONE.shiftLeft(256)))
assertEquals(BigInteger.ONE, Calldata.wrap(ByteArray(31) + byteArrayOf(1)).wordAt(0).toBigInteger())
val transient = TransientStorage(); transient.put(
Address("contract"),
Word256.ZERO,
Word256.fromLong(5)
); assertEquals(BigInteger.valueOf(5), transient.get(Address("contract"), Word256.ZERO).toBigInteger())
assertEquals(2, TwistBytecode.decode(TwistCompiler().compile("PUSH32 1\nSTOP")).size)
val folder = Files.createTempDirectory("twist-bips-test")
val storage = FileStorageContract(folder)
val hash = storage.put(byteArrayOf(4)); assertTrue(storage.prove(hash, Hash("challenge"))!!.verify())
}
@Test
fun peerAndConsensusAdaptersHaveSafeLocalFoundations() {
val dht = InMemoryDht()
val key = dht.announce(PeerId("peer-a"), setOf("memory://peer-a")); assertEquals(
PeerId("peer-a"),
dht.get(key)!!.peer
)
val adapter = TwistOverlayAdapter(TwistSpecific.Enabled, ByteArray(32) { 1 })
val message = NetworkMessage("blocks", byteArrayOf(3), Hash("block-message")); assertEquals(
message,
adapter.decode(adapter.encode(message))
)
val subject = Hash("pow-subject"); assertTrue(ProofOfWork.mine(subject, 1, 1_000)!!.verify())
assertTrue(ProofOfAuthority.sign(subject, "validator-a", setOf("validator-a")).verify())
}
@Test
fun authenticatedWireAndFileStoreRoundTrip() {
val packet = WirePacket(PacketType.PING, byteArrayOf(1, 2), flags = 5)
assertEquals(packet, PacketCodec.decode(PacketCodec.encode(packet)))
val local = X25519Session.generateKeyPair()
val remote = X25519Session.generateKeyPair()
val sender = X25519Session.establish(local.privateKey, remote.publicKey)
val receiver = X25519Session.establish(remote.privateKey, local.publicKey)
assertEquals(
"secret",
receiver.decrypt(sender.encrypt("secret".encodeToByteArray(), ByteArray(12)), ByteArray(12))
.decodeToString()
)
val root = Files.createTempDirectory("twist-store-test")
val store = FileKeyValueStore(root); store.put(byteArrayOf(7), byteArrayOf(8)); assertEquals(
8,
store.get(byteArrayOf(7))!![0]
)
}
@Test
fun cryptoMatchesPublishedVectors() {
val aes = Aes(hex("000102030405060708090a0b0c0d0e0f"))
assertEquals(
"69c4e0d86a7b0430d8cdb78070b4c55a",
aes.encryptBlock(hex("00112233445566778899aabbccddeeff")).hex()
)
val gcm = AesGcm(ByteArray(16))
val encrypted = gcm.encrypt(ByteArray(12), ByteArray(16))
assertEquals("0388dace60b6a392f328c2b971b2fe78", encrypted.ciphertext.hex())
assertEquals("ab6e47d42cec13bdf53a67b21257bddf", encrypted.tag.hex())
val scalar = hex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4")
val u = hex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c")
assertEquals(
"c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552",
X25519.scalarMult(scalar, u).hex()
)
val ecdsa = Secp256k1Ecdsa()
val privateKey = ByteArray(32).also { it[31] = 1 }
val signature = ecdsa.sign("message".encodeToByteArray(), privateKey)
assertTrue(ecdsa.verify("message".encodeToByteArray(), signature, ecdsa.publicKey(privateKey)))
assertFalse(ecdsa.verify("tampered".encodeToByteArray(), signature, ecdsa.publicKey(privateKey)))
val rsa = RsaKeyPair.fromPrimes(
BigInteger.TWO.pow(521) - BigInteger.ONE,
BigInteger.TWO.pow(607) - BigInteger.ONE
)
val ciphertext =
RsaOaep.encrypt("rsa".encodeToByteArray(), rsa.publicKey, ByteArray(32) { it.toByte() })
assertEquals("rsa", RsaOaep.decrypt(ciphertext, rsa.privateKey).decodeToString())
val damaged = ciphertext.copyOf().also { it[it.lastIndex] = (it.last().toInt() xor 1).toByte() }
assertFailsWith<IllegalArgumentException> { RsaOaep.decrypt(damaged, rsa.privateKey) }
}
@Test
fun contractIrExecutesAgainstFileBackedState() {
val ir = contract("Storage") {
function("setAndGet", 1) {
push(7)
push(42)
storageStore()
push(7)
storageLoad()
returnWord()
}
}
val state = FileWorldState(Files.createTempDirectory("twist-world-state"))
val transaction = TransactionHasher.create(
Address("caller"),
Address("storage-contract"),
0,
Amount(BigInteger.ZERO),
ir.bytecode(),
)
val result = BytecodeVirtualMachine().execute(transaction, state)
assertTrue(result.succeeded)
assertEquals(BigInteger.valueOf(42), Word256.fromBytes(result.returnData).toBigInteger())
assertNotEquals(Sha256.digest(byteArrayOf()), state.rootHash())
}
@Test
fun extendedVmMemoryAndEnvironmentInstructionsExecute() {
val code =
TwistBytecode.encode(
listOf(
Instruction(OpCode.PUSH32, Word256.ZERO.toBytes()),
Instruction(OpCode.PUSH32, Word256.fromLong(99).toBytes()),
Instruction(OpCode.MSTORE),
Instruction(OpCode.PUSH32, Word256.ZERO.toBytes()),
Instruction(OpCode.MLOAD),
Instruction(OpCode.RETURN),
)
)
val result = BytecodeVirtualMachine().execute(code, byteArrayOf(), Address("memory"))
assertTrue(result.succeeded)
assertEquals(BigInteger.valueOf(99), Word256.fromBytes(result.returnData).toBigInteger())
}
@Test
fun offchainRouterSignsProvesPersistsAndDistributesResults() {
val messages = mutableListOf<NetworkMessage>()
val network =
object : PeerNetwork {
override fun start() = Unit
override fun stop() = Unit
override fun peers() = emptySet<PeerId>()
override fun broadcast(message: NetworkMessage) {
messages += message
}
}
val store = InMemoryKeyValueStore()
val router = NetworkResultDistributor(network, store).let(::OffchainRouter)
val signer = Secp256k1Ecdsa()
val privateKey = ByteArray(32).also { it[31] = 7 }
val worker =
OffchainWorker(
Address("worker-7"),
privateKey,
signer.publicKey(privateKey),
OffchainComputation { it.reversedArray() },
)
router.register(worker)
val job =
OffchainJob(
Hash("job-1"),
Address("requester"),
"compute".encodeToByteArray(),
proofPolicy = ProofPolicy.Work(1, 10_000),
)
val result = router.route(job)
assertTrue(router.verify(job, result))
assertEquals("etupmoc", result.output.decodeToString())
assertEquals(1, messages.size)
assertNotNull(store.get(job.id.value.encodeToByteArray()))
}
@Test
fun lispIrEnforcesReadWriteAndExecuteCapabilities() {
val source =
"""
(contract Vault
(access
(read storage:* alice)
(write storage:* alice)
(execute function:set alice))
(function set 1
(push32 7)
(push32 42)
(sstore)
(return)))
""".trimIndent()
assertTrue(LispIrCompiler().compile(source, "alice").isNotEmpty())
assertFailsWith<IllegalArgumentException> { LispIrCompiler().compile(source, "mallory") }
}
@Test
fun didMethodsResolveDocuments() {
val publicKey = ByteArray(32) { it.toByte() }
val keyDid = DidKey.create(publicKey, KeyCodec.X25519)
assertEquals(keyDid, DidKey.resolve(keyDid).id)
val dht = InMemoryDht()
val documents = InMemoryKeyValueStore()
val twist = DidTwist(TwistSpecific.Enabled, dht, documents)
val twistDid =
twist.create(
PeerId("node-a"),
publicKey,
setOf("twist://node-a"),
Duration.ofMinutes(5),
)
assertEquals("twist://node-a", twist.resolve(twistDid)!!.services.single().endpoint)
assertEquals(
twistDid,
DidMethodRegistry.create(TwistSpecific.Enabled, twist).resolve(twistDid)!!.id,
)
assertFailsWith<IllegalArgumentException> { DidMethodRegistry.create(twist = twist) }
}
@Test
fun minerAndReflectorAreBoundedAndPersistent() {
assertNotNull(CpuMiner(2).mine(Hash("cpu-miner"), 1, 10_000).proof)
val store = InMemoryKeyValueStore()
val index = PeerIndex(store)
val reflector =
NetworkReflector(
PeerProbe { peer ->
PeerObservation(
peer,
setOf("memory://${peer.value}"),
if (peer.value == "a") setOf(PeerId("b")) else emptySet(),
Instant.EPOCH,
)
},
index,
)
val report = reflector.crawl(listOf(PeerId("a")), 2)
assertEquals(2, report.observations.size)
assertNotNull(index.get(PeerId("b")))
}
@Test
fun authenticatedEncryptionRejectsModification() {
val cipher = AesGcm(ByteArray(32) { it.toByte() })
val encrypted = cipher.encrypt(ByteArray(12), "authenticated".encodeToByteArray())
val altered = encrypted.ciphertext.copyOf().also { it[0] = (it[0].toInt() xor 1).toByte() }
assertFailsWith<IllegalArgumentException> {
cipher.decrypt(ByteArray(12), altered, encrypted.tag)
}
val alice = X25519Session.generateKeyPair(kotlin.random.Random(1))
val bob = X25519Session.generateKeyPair(kotlin.random.Random(2))
assertEquals(
X25519.sharedSecret(alice.privateKey, bob.publicKey).toList(),
X25519.sharedSecret(bob.privateKey, alice.publicKey).toList(),
)
}
@Test
fun twistoPersistsContentAddressedMetadataAndBalances() {
val state = InMemoryKeyValueStore()
val storage = FileStorageContract(Files.createTempDirectory("twisto-metadata"))
val owner = Address("owner")
val token = TwistoToken(owner, state, storage)
val pointer =
token.deploy(TwistoMetadata(description = "Test token"), Amount(BigInteger.TEN))
assertEquals(pointer, token.metadataPointer())
assertEquals(TwistoMetadata(description = "Test token").encode().toList(), token.metadata()!!.toList())
assertTrue(token.metadata()!!.decodeToString().contains("\"schema\":\"twisto-metadata/1\""))
assertEquals(BigInteger.TEN, token.balanceOf(owner).value)
assertFailsWith<IllegalArgumentException> {
token.mint(Address("attacker"), Address("attacker"), Amount(BigInteger.ONE))
}
}
private fun hex(value: String): ByteArray = value.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) }
}

View file

@ -0,0 +1 @@
// Configured by the root build.

View file

@ -0,0 +1,21 @@
package rip.crit.twist.tool.compiler
import java.nio.file.Files
import java.nio.file.Path
import rip.crit.twist.compiler.TwistCompiler
import rip.crit.twist.compiler.LispIrCompiler
fun main(args: Array<String>) {
require(args.size in 2..3) {
"Usage is twistc <source.twistasm|source.twistl> <output.tbc> [principal]"
}
val source = Path.of(args[0])
val text = Files.readString(source)
val bytecode =
if (source.fileName.toString().endsWith(".twistl")) {
LispIrCompiler().compile(text, args.getOrNull(2) ?: "anonymous")
} else {
TwistCompiler().compile(text)
}
Files.write(Path.of(args[1]), bytecode)
}

View file

@ -0,0 +1 @@
description = "SmartDoc static documentation generator"

View file

@ -0,0 +1,11 @@
package rip.crit.twist.tool.docs
import java.nio.file.Path
import rip.crit.twist.smartdoc.SmartDocGenerator
fun main(args: Array<String>) {
val source = Path.of(args.getOrNull(0) ?: "app/src/main/kotlin")
val output = Path.of(args.getOrNull(1) ?: "build/docs/api")
val entries = SmartDocGenerator.generate(source, output)
println("Generated ${entries.size} SmartDoc entries at $output/index.html")
}

View file

@ -0,0 +1 @@
description = "Twist CPU miner command-line tool"

View file

@ -0,0 +1,31 @@
package rip.crit.twist.tool.miner
import java.math.BigInteger
import java.nio.file.Path
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.core.Hash
import rip.crit.twist.miner.CpuMiner
import rip.crit.twist.stake.PersistentStaking
import rip.crit.twist.store.FileKeyValueStore
import rip.crit.twist.proof.ProofOfWork
import rip.crit.twist.miner.MiningResult
fun main(args: Array<String>) {
require(args.isNotEmpty()) {
"Usage is twist-miner mine <subject> <difficulty> [attempts] [threads], or stake <folder> <delegator> <validator> <amount>"
}
if (args[0] == "stake") {
require(args.size == 5)
val staking = PersistentStaking(FileKeyValueStore(Path.of(args[1])))
val validator = Address(args[3])
staking.stake(Address(args[2]), validator, Amount(BigInteger(args[4])))
println("staked validator=${validator.value} power=${staking.votingPower(validator).value}")
return
}
require(args[0] == "mine" && args.size in 3..5)
val result =
CpuMiner(args.getOrNull(4)?.toInt() ?: Runtime.getRuntime().availableProcessors())
.mine(Hash(args[1]), args[2].toInt(), args.getOrNull(3)?.toLong() ?: 1_000_000) as MiningResult
println("subject=${result.subject.value} difficulty=${result.difficulty} proof=${result.proof?.nonce} attempts=${result.attempts} elapsedNanos=${result.elapsedNanos}")
}

View file

@ -0,0 +1 @@
description = "Twist network reflection command-line tool"

View file

@ -0,0 +1,23 @@
package rip.crit.twist.tool.reflect
import java.nio.file.Path
import java.time.Instant
import rip.crit.twist.p2p.PeerId
import rip.crit.twist.reflect.NetworkReflector
import rip.crit.twist.reflect.PeerIndex
import rip.crit.twist.reflect.PeerObservation
import rip.crit.twist.reflect.PeerProbe
import rip.crit.twist.store.FileKeyValueStore
fun main(args: Array<String>) {
require(args.size >= 2) { "Usage is twist-reflect <index-folder> <seed> [seed...]" }
val index = PeerIndex(FileKeyValueStore(Path.of(args[0])))
val probe =
PeerProbe { peer ->
PeerObservation(peer, emptySet(), emptySet(), Instant.EPOCH)
}
val report = NetworkReflector(probe, index).crawl(args.drop(1).map(::PeerId))
println(
"observed=${report.observations.size} failures=${report.failures.size} truncated=${report.truncated}"
)
}

View file

@ -0,0 +1 @@
description = "Twisto deployment example"

View file

@ -0,0 +1,31 @@
package rip.crit.twist.tool.twisto
import java.math.BigInteger
import java.nio.file.Path
import rip.crit.twist.bips.FileStorageContract
import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.store.FileKeyValueStore
import rip.crit.twist.twisto.TwistoMetadata
import rip.crit.twist.twisto.TwistoToken
fun main(args: Array<String>) {
require(args.size in 1..3) {
"Usage is twisto <data-folder> [owner] [initial-supply]"
}
val root = Path.of(args[0])
val owner = Address(args.getOrNull(1) ?: "twisto-owner")
val token =
TwistoToken(
owner,
FileKeyValueStore(root.resolve("state")),
FileStorageContract(root.resolve("metadata")),
)
val pointer =
token.metadataPointer()
?: token.deploy(
TwistoMetadata(description = "Named token on the Twist network"),
Amount(BigInteger(args.getOrNull(2) ?: "1000000")),
)
println("deployed symbol=${token.symbol} metadata=${pointer.value} owner=${owner.value}")
}

186
build.gradle.kts Normal file
View file

@ -0,0 +1,186 @@
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.gradle.api.plugins.JavaApplication
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.authentication.http.BasicAuthentication
import org.gradle.api.credentials.PasswordCredentials
plugins {
kotlin("jvm") version "2.4.0" apply false
id("com.ncorti.ktfmt.gradle") version "0.27.0"
}
repositories {
mavenCentral()
}
val libraryModules = setOf(
"core", "wire", "transport", "bytecode", "compiler", "proof", "ecdsa", "rsa", "merkle", "store", "state", "access", "gas", "tvm", "jit", "ope",
"p2p", "gossip", "consensus", "mint", "burn", "bips", "standards", "stake", "router", "did", "miner", "reflect", "twisto", "smartdoc",
)
val twistVersion = providers.gradleProperty("version").orElse("0.1.0-SNAPSHOT")
allprojects {
group = "rip.crit"
version = twistVersion.get()
}
val moduleDependencies = mapOf(
"wire" to listOf("core", "p2p"),
"transport" to listOf("wire", "p2p", "core"),
"bytecode" to listOf("core"),
"compiler" to listOf("bytecode", "jit", "tvm"),
"proof" to listOf("core", "merkle"),
"ecdsa" to listOf("core"),
"rsa" to listOf("core"),
"merkle" to listOf("core"),
"store" to listOf("core"),
"state" to listOf("core"),
"access" to listOf("core"),
"tvm" to listOf("core", "gas", "state", "bytecode"),
"ope" to listOf("core", "tvm", "access"),
"p2p" to listOf("core"),
"gossip" to listOf("p2p"),
"consensus" to listOf("core"),
"mint" to listOf("core"),
"burn" to listOf("core"),
"bips" to listOf("core", "proof"),
"standards" to listOf("core", "store"),
"stake" to listOf("core", "store"),
"router" to listOf("core", "proof", "ecdsa", "p2p", "store"),
"did" to listOf("core", "p2p", "store"),
"miner" to listOf("core", "proof"),
"reflect" to listOf("core", "p2p", "store", "wire"),
"twisto" to listOf("core", "standards", "bips", "store", "compiler"),
"smartdoc" to listOf("core"),
)
ktfmt {
kotlinLangStyle()
}
tasks.register<com.ncorti.ktfmt.gradle.tasks.KtfmtCheckTask>("fmtCheck") {
group = "verification"
source = project.fileTree(layout.projectDirectory.dir("app/src/main/kotlin")) {
include("**/*.kt")
}
}
tasks.register<com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask>("fmt") {
group = "formatting"
source = project.fileTree(layout.projectDirectory.dir("app/src/main/kotlin")) {
include("**/*.kt")
}
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("projects/${project.name}"))
repositories { mavenCentral() }
extensions.configure<KotlinJvmProjectExtension> {
jvmToolchain(25)
sourceSets.named("main") {
kotlin.srcDir(rootProject.file("app/src/main/kotlin"))
kotlin.include("rip/crit/twist/${project.name}/**")
}
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.add("-Xjsr305=strict")
}
}
configure(subprojects.filter { it.name in libraryModules }) {
apply(plugin = "java-library")
apply(plugin = "maven-publish")
extensions.configure<org.gradle.api.plugins.JavaPluginExtension> {
withSourcesJar()
withJavadocJar()
}
dependencies {
moduleDependencies[name].orEmpty().forEach { dependency -> add("api", project(":$dependency")) }
}
extensions.configure<PublishingExtension> {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
artifactId = "twist-${project.name}"
pom {
name.set("Twist ${project.name}")
description.set(project.description ?: "Twist ${project.name} library")
}
}
}
repositories {
maven {
name = "forgejo"
url = uri("https://ai.crit.rip/api/packages/jprims/maven")
credentials(PasswordCredentials::class) {
username = providers.gradleProperty("forgejoUsername").orNull
password = providers.gradleProperty("forgejoPassword").orNull
}
authentication {
create<BasicAuthentication>("basic")
}
}
}
}
}
project(":app") {
apply(plugin = "application")
extensions.configure<KotlinJvmProjectExtension> {
sourceSets.named("main") { kotlin.include("rip/crit/twist/App.kt") }
}
dependencies {
libraryModules.forEach { dependency -> add("implementation", project(":$dependency")) }
}
extensions.configure<JavaApplication> {
mainClass.set("rip.crit.twist.AppKt")
}
}
project(":compiler-app") {
apply(plugin = "application")
extensions.configure<KotlinJvmProjectExtension> {
sourceSets.named("main") {
kotlin.setSrcDirs(listOf(rootProject.file("apps/compiler/src/main/kotlin")))
kotlin.setIncludes(emptySet())
}
}
dependencies { add("implementation", project(":compiler")) }
extensions.configure<JavaApplication> { mainClass.set("rip.crit.twist.tool.compiler.MainKt") }
}
fun configureTool(projectName: String, sourceDirectory: String, dependency: String, entryPoint: String) {
project(projectName) {
apply(plugin = "application")
extensions.configure<KotlinJvmProjectExtension> {
sourceSets.named("main") {
kotlin.setSrcDirs(listOf(rootProject.file(sourceDirectory)))
kotlin.setIncludes(emptySet())
}
}
dependencies { add("implementation", project(dependency)) }
extensions.configure<JavaApplication> { mainClass.set(entryPoint) }
}
}
configureTool(":miner-tool", "apps/miner/src/main/kotlin", ":miner", "rip.crit.twist.tool.miner.MainKt")
project(":miner-tool") {
dependencies {
add("implementation", project(":stake"))
add("implementation", project(":store"))
}
}
configureTool(":reflect-tool", "apps/reflect/src/main/kotlin", ":reflect", "rip.crit.twist.tool.reflect.MainKt")
configureTool(":twisto-tool", "apps/twisto/src/main/kotlin", ":twisto", "rip.crit.twist.tool.twisto.MainKt")
configureTool(":docs-tool", "apps/docs/src/main/kotlin", ":smartdoc", "rip.crit.twist.tool.docs.MainKt")

7
gradle.properties Normal file
View file

@ -0,0 +1,7 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more