diff --git a/contract_samples/assemblyscript/5_rpg/main.ts b/contract_samples/assemblyscript/5_rpg/main.ts new file mode 100644 index 0000000..052d7d9 --- /dev/null +++ b/contract_samples/assemblyscript/5_rpg/main.ts @@ -0,0 +1,80 @@ +import * as sdk from "../sdk"; +import { b, bcat, b58 } from "../sdk"; +import { Monster, Goblin, Orc, Hero, view_item } from "./model"; + +export function view_nft(): void {} + +export function init(): void { + sdk.log("RPG Inited"); + sdk.call("Nft", "create_collection", [b("RPG"), b("false")]); + sdk.call("Nft", "create_collection", [b("RPGSOULBOUND"), b("true")]); +} + +export function create_hero(): void { + const new_hero = new Hero(); + + const key = bcat([b("hero:"), sdk.account_caller()]); + assert(!sdk.kv_exists(key), "hero exists"); + + sdk.kv_put(key, new_hero.serialize()); +} + +export function fight(): void { + // load hero + const hero_key = bcat([b("hero:"), sdk.account_caller()]); + const hero_bytes = sdk.kv_get(hero_key); + assert(hero_bytes, "hero doesnt exist"); + let hero = Hero.deserialize(hero_bytes!); + + // random monster encounter + const monster_dice = roll_dice(10); + let monster: Monster = new Goblin(); + if (monster_dice >= 7) { + monster = new Orc(); + } + + // FIGHT! + while (1) { + if (hero.hp_cur <= 0) { + sdk.log("You have died, reviving"); + hero.hp_cur = hero.hp_max; + sdk.kv_put(hero_key, hero.serialize()); + break; + } + if (monster.hp_cur <= 0) { + sdk.log("Monster died"); + let dropTable = monster.getDropTable(); + for (let i = 0; i < dropTable.length; i++) { + let item = dropTable[i]; + let roll = roll_dice(100); + if (roll <= item.chance) { + sdk.log(`Monster dropped a ${item.nft_id}`); + sdk.call("Nft", "mint", [sdk.account_caller(), b("1"), b("RPG"), b(item.nft_id)]) + } + } + break; + } + + let weapon_stats = view_item(hero.weapon); + let helmet_stats = view_item(hero.helmet); + let hero_attack_roll = + roll_dice(weapon_stats.dam_dice_sides) + weapon_stats.dam_extra; + let hero_ac = helmet_stats.ac; + + let monster_attack_dice = roll_dice(monster.dam_max) + monster.dam_min; + + let hero_damage_taken = monster_attack_dice - hero_ac; + let monster_damage_taken = hero_attack_roll - monster.ac; + + hero.hp_cur -= hero_damage_taken as i16; + monster.hp_cur -= monster_damage_taken as i16; + + sdk.log(`You hit monster for ${monster_damage_taken}, mob HP is ${monster.hp_cur}/${monster.hp_max}.`); + sdk.log(`Monster hit you for ${hero_damage_taken}, your HP is ${hero.hp_cur}/${hero.hp_max}.`); + } +} + +export function roll_dice(sides: u32): i64 { + const val = Math.random(); // Returns 0.0 to 1.0 + return (floor(val * sides) as i32) + 1; // Returns 1-sides +} diff --git a/contract_samples/assemblyscript/5_rpg/model.ts b/contract_samples/assemblyscript/5_rpg/model.ts new file mode 100644 index 0000000..4f8b5d3 --- /dev/null +++ b/contract_samples/assemblyscript/5_rpg/model.ts @@ -0,0 +1,139 @@ +import { + Serializer, + Deserializer, + DecodeRef, + decodeVarint, + decodeString, + decodeU16, + decodeBytes, + TYPE_MAP, +} from "../sdk_vecpak"; + +export class Hero { + hp_cur: i16 = 20; + hp_max: i16 = 20; + str: u16 = 10; + dex: u16 = 10; + int: u16 = 10; + weapon: string = ""; + helmet: string = ""; + + serialize(): Uint8Array { + const s = new Serializer(); + + s.addI16("hp_cur", this.hp_cur); + s.addI16("hp_max", this.hp_max); + s.addU16("str", this.str); + s.addU16("dex", this.dex); + s.addU16("int", this.int); + + return s.finish(); + } + + static deserialize(data: Uint8Array): Hero { + const d = new Deserializer(data); + const hero = new Hero(); + + while (d.hasNext()) { + const key = d.nextKey(); + + if (key == "hp_cur") { + hero.hp_cur = d.readI16(); + } else if (key == "hp_max") { + hero.hp_max = d.readI16(); + } else if (key == "str") { + hero.str = d.readU16(); + } else if (key == "dex") { + hero.dex = d.readU16(); + } else if (key == "int") { + hero.int = d.readU16(); + } else { + d.skip(); + } + } + + return hero; + } +} + +export class Monster { + hp_cur: i16 = 10; + hp_max: i16 = 10; + dam_min: u16 = 1; + dam_max: u16 = 2; + ac: u16 = 0; + drop_table: Array = new Array(); + getDropTable(): StaticArray { + return []; + } +} + +export class DropChance { + constructor( + public nft_id: string = "null", + public chance: i32 = 0, + ) {} +} + +export class Goblin extends Monster { + hp_cur: i16 = 10; + hp_max: i16 = 10; + dam_min: u16 = 1; + dam_max: u16 = 2; + ac: u16 = 0; + static drop_table: StaticArray = [ + new DropChance("gold", 30), + new DropChance("rusty_dagger", 5), + ]; + getDropTable(): StaticArray { + return Goblin.drop_table; + } +} + +export class Orc extends Monster { + hp_cur: i16 = 18; + hp_max: i16 = 18; + dam_min: u16 = 1; + dam_max: u16 = 3; + ac: u16 = 1; + static drop_table: StaticArray = [ + new DropChance("gold", 30), + new DropChance("orc_helmet", 5), + ]; + getDropTable(): StaticArray { + return Orc.drop_table; + } +} + +export class Stats { + dam_dice_sides: u16 = 0; + dam_extra: u16 = 0; + ac: u16 = 0; + image_url: string = ""; +} + +export function view_item(nft_id: string): Stats { + switch (nft_id) { + case "gold": + let stats = new Stats(); + stats.image_url = + "https://ipfs.io/ipfs/QmWBaeu6y1zEcKbsEqCuhuDHPL3W8pZouCPdafMCRCSUWk"; + return stats; + case "rusty_dagger": + let stats = new Stats(); + stats.dam_dice_sides = 3; + stats.dam_extra = 1; + stats.image_url = + "https://ipfs.io/ipfs/QmWBaeu6y1zEcKbsEqCuhuDHPL3W8pZouCPdafMCRCSUWk"; + return stats; + case "orc_helmet": + let stats = new Stats(); + stats.ac = 1; + stats.image_url = + "https://ipfs.io/ipfs/QmWBaeu6y1zEcKbsEqCuhuDHPL3W8pZouCPdafMCRCSUWk"; + return stats; + default: + let stats = new Stats(); + return stats; + } +} diff --git a/contract_samples/assemblyscript/sdk_vecpak.ts b/contract_samples/assemblyscript/sdk_vecpak.ts new file mode 100644 index 0000000..15eaff1 --- /dev/null +++ b/contract_samples/assemblyscript/sdk_vecpak.ts @@ -0,0 +1,330 @@ +export const TYPE_NULL: u8 = 0x00; +export const TYPE_TRUE: u8 = 0x01; +export const TYPE_FALSE: u8 = 0x02; +export const TYPE_INT: u8 = 0x03; +export const TYPE_BYTES: u8 = 0x05; +export const TYPE_LIST: u8 = 0x06; +export const TYPE_MAP: u8 = 0x07; + +// --- 2. Helper Classes --- + +export class DecodeRef { + offset: i32 = 0; +} + +class Field { + constructor( + public key: string, + public keyBytes: Uint8Array, + public valueBytes: Uint8Array, + ) {} +} + +// --- 3. Encoding Functions (Low Level) --- + +export function encodeVarint(n: i64, out: Array): void { + if (n == 0) { + out.push(0); + return; + } + + const isNegative = n < 0; + let value: u64 = isNegative ? -n : n; + + // Build magnitude in little-endian first + const magBytes = new Array(); + while (value > 0) { + magBytes.push((value & 0xff)); + value = value >> 8; + } + // Reverse to get big-endian + magBytes.reverse(); + + const len = magBytes.length; + // Header: sign bit (bit 7) | length (bits 0-6) + const header = (((isNegative ? 1 : 0) << 7) | len); + + out.push(header); + for (let i = 0; i < len; i++) { + out.push(magBytes[i]); + } +} + +export function encodeU16(val: u16, out: Array): void { + out.push(TYPE_INT); + encodeVarint(val, out); +} + +export function encodeI16(val: i16, out: Array): void { + out.push(TYPE_INT); + // Casting i16 to i64 preserves the sign automatically in AS + encodeVarint(val, out); +} + +export function encodeString(val: string, out: Array): void { + out.push(TYPE_BYTES); + + const utf8 = String.UTF8.encode(val); + encodeVarint(utf8.byteLength, out); + + // ArrayBuffer to Array + const view = new DataView(utf8); + for (let i = 0; i < utf8.byteLength; i++) { + out.push(view.getUint8(i)); + } +} + +export function encodeBytes(val: Uint8Array, out: Array): void { + out.push(TYPE_BYTES); + encodeVarint(val.length, out); + + for (let i = 0; i < val.length; i++) { + out.push(val[i]); + } +} + +// --- 4. Decoding Functions (Low Level) --- + +export function decodeVarint(data: Uint8Array, ref: DecodeRef): i64 { + if (ref.offset >= data.length) throw new Error("EOF reading varint"); + + const header = data[ref.offset]; + ref.offset++; + + if (header == 0) return 0; + + const signBit = header >> 7; + // FIX: Cast the result to i32 immediately + const length = (header & 0x7f); + + let mag: u64 = 0; + + // Now comparing i32 < i32, which is valid + for (let i = 0; i < length; i++) { + if (ref.offset >= data.length) throw new Error("EOF reading varint bytes"); + mag = (mag << 8) | (data[ref.offset]); + ref.offset++; + } + + return signBit == 1 ? -(mag) : mag; +} + +export function decodeU16(data: Uint8Array, ref: DecodeRef): u16 { + if (ref.offset >= data.length) throw new Error("EOF reading type"); + if (data[ref.offset] != TYPE_INT) throw new Error("Expected TYPE_INT"); + ref.offset++; + return decodeVarint(data, ref); +} + +export function decodeI16(data: Uint8Array, ref: DecodeRef): i16 { + if (ref.offset >= data.length) throw new Error("EOF reading type"); + if (data[ref.offset] != TYPE_INT) throw new Error("Expected TYPE_INT"); + ref.offset++; + // Cast the i64 result down to i16 + return decodeVarint(data, ref); +} + +export function decodeString(data: Uint8Array, ref: DecodeRef): string { + if (ref.offset >= data.length) throw new Error("EOF reading type"); + if (data[ref.offset] != TYPE_BYTES) + throw new Error("Expected TYPE_BYTES for string"); + ref.offset++; + + const len = decodeVarint(data, ref); + const start = ref.offset; + const end = start + len; + + if (end > data.length) throw new Error("EOF reading string bytes"); + + // Slice buffer + const buffer = data.buffer.slice( + data.byteOffset + start, + data.byteOffset + end, + ); + ref.offset += len; + return String.UTF8.decode(buffer); +} + +export function decodeBytes(data: Uint8Array, ref: DecodeRef): Uint8Array { + if (ref.offset >= data.length) throw new Error("EOF reading type"); + if (data[ref.offset] != TYPE_BYTES) + throw new Error("Expected TYPE_BYTES for Uint8Array"); + ref.offset++; + + const len = decodeVarint(data, ref); + const start = ref.offset; + const end = start + len; + + if (end > data.length) throw new Error("EOF reading bytes"); + + // Create a copy for the result + const result = new Uint8Array(len); + for (let i = 0; i < len; i++) { + result[i] = data[start + i]; + } + + ref.offset += len; + return result; +} + +// --- 5. High-Level Serializer (Sorting & Map Construction) --- + +export class Serializer { + private fields: Array = new Array(); + + // Generic internal helper + private addFieldRaw(key: string, valueBytes: Uint8Array): void { + // We must encode the key to bytes to sort canonically + const keyOut = new Array(); + encodeString(key, keyOut); + + const keyUint8 = new Uint8Array(keyOut.length); + for (let i = 0; i < keyOut.length; i++) keyUint8[i] = keyOut[i]; + + this.fields.push(new Field(key, keyUint8, valueBytes)); + } + + // Helper to convert Array -> Uint8Array + private toUint8(arr: Array): Uint8Array { + const res = new Uint8Array(arr.length); + for (let i = 0; i < arr.length; i++) res[i] = arr[i]; + return res; + } + + // --- Public API --- + + addU16(key: string, val: u16): void { + const out = new Array(); + encodeU16(val, out); + this.addFieldRaw(key, this.toUint8(out)); + } + + addI16(key: string, val: i16): void { + const out = new Array(); + encodeI16(val, out); + this.addFieldRaw(key, this.toUint8(out)); + } + + addString(key: string, val: string): void { + const out = new Array(); + encodeString(val, out); + this.addFieldRaw(key, this.toUint8(out)); + } + + addBytes(key: string, val: Uint8Array): void { + const out = new Array(); + encodeBytes(val, out); + this.addFieldRaw(key, this.toUint8(out)); + } + + finish(): Uint8Array { + // 1. Canonical Sort: Compare raw key bytes + this.fields.sort((a, b) => { + const len = Math.min(a.keyBytes.length, b.keyBytes.length); + for (let i = 0; i < len; i++) { + if (a.keyBytes[i] != b.keyBytes[i]) { + return a.keyBytes[i] - b.keyBytes[i]; + } + } + return a.keyBytes.length - b.keyBytes.length; + }); + + // 2. Construct final bytes + const finalOut = new Array(); + + // Map Header + finalOut.push(TYPE_MAP); + encodeVarint(this.fields.length, finalOut); + + for (let i = 0; i < this.fields.length; i++) { + const f = this.fields[i]; + // Key + for (let k = 0; k < f.keyBytes.length; k++) finalOut.push(f.keyBytes[k]); + // Value + for (let v = 0; v < f.valueBytes.length; v++) + finalOut.push(f.valueBytes[v]); + } + + return this.toUint8(finalOut); + } +} + +export class Deserializer { + private ref: DecodeRef = new DecodeRef(); + private count: i64 = 0; + private current: i64 = 0; + + constructor(public data: Uint8Array) { + // Automatically parse the MAP header + if (this.ref.offset >= data.length) throw new Error("Empty data"); + if (data[this.ref.offset] != TYPE_MAP) + throw new Error("Expected Map Header"); + this.ref.offset++; + + // Read the number of fields + this.count = decodeVarint(data, this.ref); + } + + hasNext(): boolean { + return this.current < this.count; + } + + nextKey(): string { + if (!this.hasNext()) throw new Error("No more fields"); + this.current++; + // Keys are always encoded as strings in this format + return decodeString(this.data, this.ref); + } + + // --- Type Readers --- + + readU16(): u16 { + return decodeU16(this.data, this.ref); + } + + readI16(): i16 { + return decodeI16(this.data, this.ref); + } + + readBytes(): Uint8Array { + return decodeBytes(this.data, this.ref); + } + + // Allows you to ignore fields you don't know about (Forward Compatibility) + skip(): void { + if (this.ref.offset >= this.data.length) return; + + const type = this.data[this.ref.offset]; + + // We don't increment offset here; the specific decode functions + // inside the switch will handle the type byte + payload. + + // Note: We "peek" the type by reading the byte, but standard decode functions + // (like decodeU16) usually consume the type byte. + // However, for raw skipping, we need to handle the structure manually. + + // Logic: Read type byte -> Advance logic + this.ref.offset++; // consume type header + + switch (type) { + case TYPE_NULL: + case TYPE_TRUE: + case TYPE_FALSE: + // No payload + break; + case TYPE_INT: + // Just read the varint and ignore result + decodeVarint(this.data, this.ref); + break; + case TYPE_BYTES: { + const len = decodeVarint(this.data, this.ref); + this.ref.offset += len; // Jump over bytes + break; + } + // TODO: Recursive skip for TYPE_LIST and TYPE_MAP if needed. + // For flat objects like Hero, this is not needed. + default: + throw new Error("Cannot skip unknown type: " + type.toString()); + } + } +} diff --git a/ex/config/runtime.exs b/ex/config/runtime.exs index ab9fc72..b021ad6 100644 --- a/ex/config/runtime.exs +++ b/ex/config/runtime.exs @@ -29,6 +29,8 @@ config :ama, :snapshot_height, (System.get_env("SNAPSHOT_HEIGHT") || "43401193") #Bind Interaces config :ama, :offline, (!!System.get_env("OFFLINE") || nil) config :ama, :testnet, (!!System.get_env("TESTNET") || nil) +testnet_sleep_default = if !!System.get_env("TESTNET") do "350" else "0" end +config :ama, :testnet_sleep, (System.get_env("TESTNET_SLEEP") || testnet_sleep_default) |> :erlang.binary_to_integer() config :ama, :http_ipv4, ((System.get_env("HTTP_IPV4") || "0.0.0.0") |> :unicode.characters_to_list() |> :inet.parse_ipv4_address() |> (case do {:ok, addr}-> addr end)) config :ama, :http_port, (System.get_env("HTTP_PORT") || "80") |> :erlang.binary_to_integer() diff --git a/ex/lib/api/api_tx.ex b/ex/lib/api/api_tx.ex index 718ef82..f659c89 100644 --- a/ex/lib/api/api_tx.ex +++ b/ex/lib/api/api_tx.ex @@ -173,6 +173,9 @@ defmodule API.TX do tx = put_in(tx, [:tx, :signer], Base58.encode(tx.tx.signer)) action = TX.action(tx) + action = if !BlsEx.validate_public_key(action.contract) do action else + Map.put(action, :contract, Base58.encode(action.contract)) + end args = Enum.map(action.args, fn(arg)-> cond do !is_binary(arg) or Util.ascii?(arg) -> arg diff --git a/ex/lib/api/api_wallet.ex b/ex/lib/api/api_wallet.ex index 9ea97cf..8bd088c 100644 --- a/ex/lib/api/api_wallet.ex +++ b/ex/lib/api/api_wallet.ex @@ -27,6 +27,17 @@ defmodule API.Wallet do %{collection: collection, token: token, amount: amount} end + def balance_nft_all(pk) do + pk = API.maybe_b58(48, pk) + %{db: db, cf: cf} = :persistent_term.get({:rocksdb, Fabric}) + opts = %{db: db, cf: cf.contractstate, to_integer: true} + RocksDB.get_prefix("account:#{pk}:nft:", opts) + |> Enum.map(fn({symbol, amount})-> + [collection, token] = :binary.split(symbol, ":") + %{collection: collection, token: token, amount: amount} + end) + end + def transfer(to, amount, symbol) do sk = Application.fetch_env!(:ama, :trainer_sk) transfer(sk, to, amount, symbol) diff --git a/ex/lib/consensus/fabric_gen.ex b/ex/lib/consensus/fabric_gen.ex index 32dba99..b607bec 100644 --- a/ex/lib/consensus/fabric_gen.ex +++ b/ex/lib/consensus/fabric_gen.ex @@ -50,7 +50,8 @@ defmodule FabricGen do def tick_slot(state) do #IO.inspect "tick_slot" - Application.fetch_env!(:ama, :testnet) && Process.sleep(350) + sleep = Application.fetch_env!(:ama, :testnet_sleep) + if sleep > 0 do Process.sleep(sleep) end if proc_if_my_slot() do proc_entries() diff --git a/ex/lib/http/multiserver.ex b/ex/lib/http/multiserver.ex index dd6e01a..fb312a4 100644 --- a/ex/lib/http/multiserver.ex +++ b/ex/lib/http/multiserver.ex @@ -74,7 +74,7 @@ defmodule Ama.MultiServer do def handle_http(state) do r = state.request #IO.inspect r.path - + testnet = Application.fetch_env!(:ama, :testnet) cond do r.method in ["OPTIONS", "HEAD"] -> :ok = :gen_tcp.send(state.socket, Photon.HTTP.Response.build_cors(state.request, 200, %{}, "")) @@ -291,6 +291,43 @@ defmodule Ama.MultiServer do result = API.Proof.validators(map.key, map[:value]) quick_reply(%{state|request: r}, result) + testnet and r.method == "GET" and r.path == "/api/upow/seed" -> + epoch = DB.Chain.epoch() + segment_vr_hash = DB.Chain.segment_vr_hash() + nonce = :crypto.strong_rand_bytes(12) + %{pk: pk, pop: pop} = Application.fetch_env!(:ama, :keys) |> hd() + seed = <> + quick_reply(state, seed) + + testnet and r.method == "GET" and r.path == "/api/upow/seed_with_matrix_a_b" -> + epoch = DB.Chain.epoch() + segment_vr_hash = DB.Chain.segment_vr_hash() + nonce = :crypto.strong_rand_bytes(12) + %{pk: pk, pop: pop} = Application.fetch_env!(:ama, :keys) |> hd() + seed = <> + b = Blake3.new() + Blake3.update(b, seed) + matrix_a_b = Blake3.finalize_xof(b, 16*50240 + 50240*16) + quick_reply(state, seed <> matrix_a_b) + + testnet and r.method == "GET" and String.starts_with?(r.path, "/api/upow/validate/") -> + sol = String.replace(r.path, "/api/upow/validate/", "") |> Base58.decode() + diff_bits = DB.Chain.diff_bits() + segment_vr_hash = DB.Chain.segment_vr_hash() + result = try do BIC.Sol.verify(sol, %{diff_bits: diff_bits, segment_vr_hash: segment_vr_hash}) catch _,_ -> false end + result_math = RDB.freivalds(sol, :crypto.strong_rand_bytes(32)) + quick_reply(state, %{valid: result, valid_math: result_math}) + + testnet and r.method == "POST" and r.path == "/api/upow/validate" -> + {r, sol} = Photon.HTTP.read_body_all(state.socket, r) + diff_bits = DB.Chain.diff_bits() + segment_vr_hash = DB.Chain.segment_vr_hash() + result = try do BIC.Sol.verify(sol, %{diff_bits: diff_bits, segment_vr_hash: segment_vr_hash}) catch _,_ -> false end + result_math = RDB.freivalds(sol, :crypto.strong_rand_bytes(32)) + quick_reply(%{state|request: r}, %{valid: result, valid_math: result_math}) + #r.method == "GET" -> # bin = build_dashboard(state) # quick_reply(state, bin) diff --git a/ex/lib/misc/testnet_https.ex b/ex/lib/misc/testnet_https.ex index 1048cd0..f0bac45 100644 --- a/ex/lib/misc/testnet_https.ex +++ b/ex/lib/misc/testnet_https.ex @@ -90,19 +90,28 @@ defmodule TestNetHTTPSProxy do end def handle_info(:accept, state) do - {:ok, ssl_socket} = :ssl.transport_accept(state.listen_socket) - case :ssl.handshake(ssl_socket) do + case :ssl.transport_accept(state.listen_socket) do + {:error, _reason} -> + {:noreply, state} {:ok, ssl_socket} -> - pid = :erlang.spawn(__MODULE__, :client_loop, [%{ssl_socket: ssl_socket}]) + pid = :erlang.spawn(__MODULE__, :handshake, [%{ssl_socket: ssl_socket}]) :ok = :ssl.controlling_process(ssl_socket, pid) - {:error, reason} -> - :ssl.close(ssl_socket) + {:noreply, state} end - - :erlang.send_after(1, self(), :accept) + :erlang.send_after(0, self(), :accept) {:noreply, state} end + def handshake(state) do + Process.sleep(100) + case :ssl.handshake(state.ssl_socket) do + {:error, reason} -> + :ssl.close(state.ssl_socket) + {:ok, ssl_socket} -> + client_loop(Map.put(state, :ssl_socket, ssl_socket)) + end + end + def client_loop(state) do up_host = Application.fetch_env!(:ama, :http_ipv4) up_port = 80 diff --git a/ex/native/rdb/Cargo.lock b/ex/native/rdb/Cargo.lock index 1031ab9..da0d437 100644 --- a/ex/native/rdb/Cargo.lock +++ b/ex/native/rdb/Cargo.lock @@ -38,6 +38,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arbitrary" version = "1.4.2" @@ -209,6 +215,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytecheck" version = "0.6.12" @@ -323,6 +335,26 @@ dependencies = [ "cc", ] +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -483,6 +515,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -708,6 +746,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + [[package]] name = "flate2" version = "1.1.5" @@ -799,6 +849,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "hash-db" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7d7786361d7425ae2fe4f9e407eb0efaa0840f5212d109cc018c40c35c6ab4" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -853,6 +918,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -910,6 +995,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak-hasher" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ea4653859ca2266a86419d3f592d3f22e7a854b482f99180d2498507902048" +dependencies = [ + "hash-db", + "hash256-std-hasher", + "tiny-keccak", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1174,6 +1270,34 @@ dependencies = [ "group", ] +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1234,6 +1358,26 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1335,16 +1479,37 @@ dependencies = [ "ptr_meta 0.3.1", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1360,6 +1525,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] [[package]] name = "rand_core" @@ -1394,21 +1562,27 @@ dependencies = [ name = "rdb" version = "0.1.0" dependencies = [ + "anyhow", "atoi", "blake3", "bls12_381", "blst", "bs58", "group", + "hex", + "keccak-hasher", "lazy_static", - "rand", + "primitive-types", + "rand 0.9.2", "rayon", + "rlp", "rust-librocksdb-sys", "rust-rocksdb", "rustler", "serde", "sha2", "thiserror 2.0.17", + "trie-db", "vecpak", "vecpak_ex", "wasmer", @@ -1524,6 +1698,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "rust-librocksdb-sys" version = "0.39.0+10.5.1" @@ -1566,6 +1750,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustix" version = "1.1.2" @@ -1844,6 +2034,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -1859,6 +2058,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.43" @@ -1890,6 +2119,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trie-db" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7795f2df2ef744e4ffb2125f09325e60a21d305cc3ecece0adeef03f7a9e560" +dependencies = [ + "hash-db", + "log", + "rustc-hex", + "smallvec", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -1906,6 +2147,18 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unicode-ident" version = "1.0.19" @@ -2427,6 +2680,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/ex/native/rdb/Cargo.toml b/ex/native/rdb/Cargo.toml index 216e40d..5d8fdea 100644 --- a/ex/native/rdb/Cargo.toml +++ b/ex/native/rdb/Cargo.toml @@ -32,6 +32,12 @@ wasmer = "6.1.0" wasmer-compiler-singlepass = "6.1.0" wasmer-middlewares = "6.1.0" lazy_static = "1.4" +hex = "0.4" +rlp = "0.5" +keccak-hasher = "0.16" +trie-db = "0.31" +primitive-types = "0.13" +anyhow = "1.0" [dependencies.rust-rocksdb] git = "https://github.com/vans163/rust-rocksdb" diff --git a/ex/native/rdb/src/consensus/bic/lockup_prime.rs b/ex/native/rdb/src/consensus/bic/lockup_prime.rs index bceaa0a..536fd7e 100644 --- a/ex/native/rdb/src/consensus/bic/lockup_prime.rs +++ b/ex/native/rdb/src/consensus/bic/lockup_prime.rs @@ -1,6 +1,6 @@ use std::panic::panic_any; use crate::{bcat}; -use crate::consensus::{bic::{coin::{balance, mint, to_flat}, epoch::TREASURY_DONATION_ADDRESS, lockup::{create_lock}}, consensus_kv::{kv_get, kv_increment, kv_put, kv_delete}}; +use crate::consensus::{bic::{coin::{balance, mint, to_flat}, epoch::TREASURY_DONATION_ADDRESS, lockup::{create_lock}}, consensus_kv::{kv_get, kv_get_next, kv_increment, kv_put, kv_delete}}; use vecpak::{encode, Term}; pub fn call_lock(env: &mut crate::consensus::consensus_apply::ApplyEnv, args: Vec>) { @@ -23,8 +23,6 @@ pub fn call_lock(env: &mut crate::consensus::consensus_apply::ApplyEnv, args: Ve let amount = std::str::from_utf8(&amount).ok().and_then(|s| s.parse::().ok()).unwrap_or_else(|| panic_any("invalid_amount")); let tier = args[1].as_slice(); let (tier_epochs, multiplier) = match args.get(1).map(|v| v.as_slice()).unwrap_or_else(|| panic_any("invalid_tier")) { - b"magic" => (0, 1), - b"magic2" => (1, 1), b"7d" => (10, 13), b"30d" => (45, 17), b"90d" => (135, 27), @@ -83,16 +81,23 @@ pub fn call_unlock(env: &mut crate::consensus::consensus_apply::ApplyEnv, args: } pub fn call_daily_checkin(env: &mut crate::consensus::consensus_apply::ApplyEnv, args: Vec>) { - if args.len() != 1 { panic_any("invalid_args") } - let vault_index = args[0].as_slice(); + let prefix = bcat(&[b"bic:lockup_prime:vault:", &env.caller_env.account_caller, b":"]); + let mut cursor: Vec = Vec::new(); + let mut total_unlock_amount: u64 = 0; + let mut found_vaults = false; + + while let Some((next_key_suffix, val)) = kv_get_next(env, &prefix, &cursor) { + found_vaults = true; + let vault_parts: Vec> = val.split(|&b| b == b'-').map(|seg| seg.to_vec()).collect(); + let unlock_amount = vault_parts[3].as_slice(); + let unlock_amount = std::str::from_utf8(&unlock_amount).ok().and_then(|s| s.parse::().ok()).unwrap_or_else(|| panic_any("invalid_unlock_amount")); + total_unlock_amount += unlock_amount; + cursor = next_key_suffix; + } - let vault_key = &bcat(&[b"bic:lockup_prime:vault:", &env.caller_env.account_caller, b":", vault_index]); - let vault = kv_get(env, vault_key); - if vault.is_none() { panic_any("invalid_vault") } - let vault = vault.unwrap(); - let vault_parts: Vec> = vault.split(|&b| b == b'-').map(|seg| seg.to_vec()).collect(); - let unlock_amount = vault_parts[3].as_slice(); - let unlock_amount = std::str::from_utf8(&unlock_amount).ok().and_then(|s| s.parse::().ok()).unwrap_or_else(|| panic_any("invalid_unlock_amount")); + if !found_vaults { + panic_any("no_vaults_found"); + } let next_checkin_epoch: u64 = kv_get(env, &bcat(&[b"bic:lockup_prime:next_checkin_epoch:", &env.caller_env.account_caller])) .map(|bytes| { std::str::from_utf8(&bytes).ok().and_then(|s| s.parse::().ok()).unwrap_or_else(|| panic_any("invalid_next_checkin_epoch")) }) @@ -101,7 +106,7 @@ pub fn call_daily_checkin(env: &mut crate::consensus::consensus_apply::ApplyEnv, if delta == 0 || delta == 1 { kv_put(env, &bcat(&[b"bic:lockup_prime:next_checkin_epoch:", &env.caller_env.account_caller]), env.caller_env.entry_epoch.saturating_add(2).to_string().as_bytes()); - let daily_bonus = unlock_amount / 100; + let daily_bonus = total_unlock_amount / 100; mint(env, env.caller_env.account_caller.to_vec().as_slice(), daily_bonus as i128, b"PRIME"); let streak = kv_increment(env, &bcat(&[b"bic:lockup_prime:daily_streak:", &env.caller_env.account_caller]), 1); diff --git a/ex/native/rdb/src/consensus/bic/protocol.rs b/ex/native/rdb/src/consensus/bic/protocol.rs index f77d9f3..2b24a32 100644 --- a/ex/native/rdb/src/consensus/bic/protocol.rs +++ b/ex/native/rdb/src/consensus/bic/protocol.rs @@ -18,8 +18,27 @@ pub const COST_PER_OP_WASM: i128 = 1; //cost to execute a wasm op pub const COST_PER_DB_READ_BASE: i128 = 5_000 * 10; pub const COST_PER_DB_READ_BYTE: i128 = 50 * 10; +pub const COST_PER_DB_READ_BYTE2: i128 = 50; + pub const COST_PER_DB_WRITE_BASE: i128 = 25_000 * 10; pub const COST_PER_DB_WRITE_BYTE: i128 = 250 * 10; +pub const COST_PER_DB_WRITE_BYTE2: i128 = 250; + +pub fn cost_db_read_byte(env: &crate::consensus::consensus_apply::ApplyEnv) -> i128 { + if env.testnet { + COST_PER_DB_READ_BYTE2 + } else { + COST_PER_DB_READ_BYTE + } +} + +pub fn cost_db_write_byte(env: &crate::consensus::consensus_apply::ApplyEnv) -> i128 { + if env.testnet { + COST_PER_DB_WRITE_BYTE2 + } else { + COST_PER_DB_WRITE_BYTE + } +} pub const COST_PER_CALL: i128 = AMA_01_CENT; pub const COST_PER_DEPLOY: i128 = AMA_1_CENT; //cost to deploy contract diff --git a/ex/native/rdb/src/consensus/consensus_kv.rs b/ex/native/rdb/src/consensus/consensus_kv.rs index ea50b74..bf93000 100644 --- a/ex/native/rdb/src/consensus/consensus_kv.rs +++ b/ex/native/rdb/src/consensus/consensus_kv.rs @@ -61,7 +61,7 @@ pub fn kv_put(env: &mut ApplyEnv, key: &[u8], value: &[u8]) { } exec_kv_size(key, Some(value)); - exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::COST_PER_DB_WRITE_BYTE * (key.len() + value.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::cost_db_write_byte(env) * (key.len() + value.len()) as i128); let old_value = env.txn.get_cf(&env.cf, key).unwrap(); match old_value { @@ -90,7 +90,7 @@ pub fn kv_increment(env: &mut ApplyEnv, key: &[u8], value: i128) -> i128 { } let value_str = value.to_string().into_bytes(); - exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::COST_PER_DB_WRITE_BYTE * (key.len() + value_str.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::cost_db_write_byte(env) * (key.len() + value_str.len()) as i128); match env.txn.get_cf(&env.cf, key).unwrap() { None => { @@ -121,7 +121,7 @@ pub fn kv_delete(env: &mut ApplyEnv, key: &[u8]) { panic!("exec_cannot_write_during_view"); } - exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::COST_PER_DB_WRITE_BYTE * (key.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::cost_db_write_byte(env) * (key.len()) as i128); match env.txn.get_cf(&env.cf, key).unwrap() { None => (), @@ -138,7 +138,7 @@ pub fn kv_set_bit(env: &mut ApplyEnv, key: &[u8], bit_idx: u64) -> bool { panic!("exec_cannot_write_during_view"); } - exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::COST_PER_DB_WRITE_BYTE * (key.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_WRITE_BASE + protocol::cost_db_write_byte(env) * (key.len()) as i128); let (mut old, exists) = match env.txn.get_cf(&env.cf, key).unwrap() { None => (vec![0u8; crate::consensus::bic::sol_bloom::PAGE_SIZE as usize], false), @@ -164,7 +164,7 @@ pub fn kv_set_bit(env: &mut ApplyEnv, key: &[u8], bit_idx: u64) -> bool { } pub fn kv_exists(env: &mut ApplyEnv, key: &[u8]) -> bool { - exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::COST_PER_DB_READ_BYTE * (key.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::cost_db_read_byte(env) * (key.len()) as i128); match env.txn.get_cf(&env.cf, key).unwrap() { None => false, @@ -173,13 +173,13 @@ pub fn kv_exists(env: &mut ApplyEnv, key: &[u8]) -> bool { } pub fn kv_get(env: &mut ApplyEnv, key: &[u8]) -> Option> { - exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::COST_PER_DB_READ_BYTE * (key.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::cost_db_read_byte(env) * (key.len()) as i128); env.txn.get_cf(&env.cf, key).unwrap() } pub fn kv_get_next(env: &mut ApplyEnv, prefix: &[u8], key: &[u8]) -> Option<(Vec, Vec)> { - exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::COST_PER_DB_READ_BYTE * (prefix.len() + key.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::cost_db_read_byte(env) * (prefix.len() + key.len()) as i128); let seek = [prefix, key].concat(); @@ -203,7 +203,7 @@ pub fn kv_get_next(env: &mut ApplyEnv, prefix: &[u8], key: &[u8]) -> Option<(Vec } pub fn kv_get_prev(env: &mut ApplyEnv, prefix: &[u8], key: &[u8]) -> Option<(Vec, Vec)> { - exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::COST_PER_DB_READ_BYTE * (prefix.len() + key.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::cost_db_read_byte(env) * (prefix.len() + key.len()) as i128); let seek = [prefix, key].concat(); @@ -227,7 +227,7 @@ pub fn kv_get_prev(env: &mut ApplyEnv, prefix: &[u8], key: &[u8]) -> Option<(Vec } pub fn kv_get_prev_or_first(env: &mut ApplyEnv, prefix: &[u8], key: &[u8]) -> Option<(Vec, Vec)> { - exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::COST_PER_DB_READ_BYTE * (prefix.len() + key.len()) as i128); + exec_budget_decr(env, protocol::COST_PER_DB_READ_BASE + protocol::cost_db_read_byte(env) * (prefix.len() + key.len()) as i128); let seek = [prefix, key].concat(); diff --git a/ex/native/rdb/src/lib.rs b/ex/native/rdb/src/lib.rs index bfc7b0e..1ff7c18 100644 --- a/ex/native/rdb/src/lib.rs +++ b/ex/native/rdb/src/lib.rs @@ -1,8 +1,10 @@ pub mod consensus; pub mod atoms; pub mod model; +pub mod mpt; pub mod tx_filter; + use rustler::types::{Binary, OwnedBinary}; use rustler::{ Encoder, Error, Env, Term, NifResult, ResourceArc, Atom, @@ -11,7 +13,7 @@ use rustler::{ pub use rust_rocksdb::{TransactionDB, MultiThreaded, TransactionDBOptions, Options, Transaction, TransactionOptions, WriteOptions, CompactOptions, BottommostLevelCompaction, - DBRawIteratorWithThreadMode, BoundColumnFamily, ReadOptions, SliceTransform, + DBRawIteratorWithThreadMode, BoundColumnFamily, ReadOptions, Cache, LruCacheOptions, BlockBasedOptions, DBCompressionType, BlockBasedIndexType, ColumnFamilyDescriptor, AsColumnFamilyRef}; @@ -193,12 +195,11 @@ fn open_transaction_db<'a>(env: Env<'a>, path: String, cf_names: Vec) -> let mut block_based_options = BlockBasedOptions::default(); block_based_options.set_block_cache(&block_cache); - block_based_options.set_bloom_filter(10.0, false); block_based_options.set_index_type(BlockBasedIndexType::TwoLevelIndexSearch); + block_based_options.set_partition_filters(true); block_based_options.set_cache_index_and_filter_blocks(true); block_based_options.set_cache_index_and_filter_blocks_with_high_priority(true); block_based_options.set_pin_top_level_index_and_filter(true); - block_based_options.set_partition_filters(true); block_based_options.set_pin_l0_filter_and_index_blocks_in_cache(false); cf_opts.set_block_based_table_factory(&block_based_options); @@ -243,20 +244,7 @@ fn open_transaction_db<'a>(env: Env<'a>, path: String, cf_names: Vec) -> let cf_descriptors: Vec<_> = cf_names .iter() - .map(|name| { - let mut opts = cf_opts.clone(); - - if name == "tx" { - opts.set_prefix_extractor(SliceTransform::create_fixed_prefix(8)); - opts.set_memtable_prefix_bloom_ratio(0.1); - } - if name == "tx_filter" { - opts.set_prefix_extractor(SliceTransform::create_fixed_prefix(16)); - opts.set_memtable_prefix_bloom_ratio(0.1); - } - - ColumnFamilyDescriptor::new(name.as_str(), opts) - }) + .map(|name| ColumnFamilyDescriptor::new(name.as_str(), cf_opts.clone())) .collect(); match TransactionDB::open_cf_descriptors(&db_opts, &txn_db_opts, Path::new(&path), cf_descriptors) { @@ -452,18 +440,6 @@ fn delete_cf(cf: ResourceArc, key: Binary) -> NifResult { .map_err(to_nif_rdb_err) } -#[rustler::nif] -fn delete_range_cf(cf: ResourceArc, start_key: Binary, end_key: Binary, compact: bool) -> NifResult { - cf.db.db - .delete_range_cf(&*cf, start_key.as_slice(), end_key.as_slice()) - .map(|_| atoms::ok()) - .map_err(to_nif_rdb_err); - if compact { - cf.db.db.compact_range_cf(&*cf, Option::<&[u8]>::None, Option::<&[u8]>::None); - } - Ok(atoms::ok()) -} - #[rustler::nif] fn iterator<'a>(env: Env<'a>, db: ResourceArc) -> NifResult> { let res = ItResource::new(db.clone(), None, None); @@ -1059,20 +1035,46 @@ fn protocol_circulating_without_burn<'a>(env: Env<'a>, epoch: u64) -> i128 { } #[rustler::nif] +fn mpt_verify_proof<'a>(env: Env<'a>, root: Binary, index: Term<'a>, proof: Vec) -> NifResult> { + use crate::mpt; + + // Validate root is 32 bytes + if root.len() != 32 { + return Err(Error::Term(Box::new(format!("Root must be 32 bytes, got {}", root.len())))); + } + + // Parse index - can be either a number (u64) or already RLP-encoded bytes + let index_bytes = if let Ok(num) = index.decode::() { + mpt::rlp_encode_number(num) + } else if let Ok(bin) = index.decode::() { + bin.as_slice().to_vec() + } else { + return Err(Error::Term(Box::new("Index must be a number (u64) or binary (RLP-encoded)".to_string()))); + }; + + // Convert proof nodes to Vec> + let proof_nodes = proof.iter().map(|b| b.as_slice()); + + // Verify the proof + match mpt::verify_proof(root.as_slice(), &index_bytes, proof_nodes) { + Ok(value) => { + let mut ob = OwnedBinary::new(value.len()) + .ok_or_else(|| Error::Term(Box::new("alloc failed")))?; + ob.as_mut_slice().copy_from_slice(&value); + Ok((atoms::ok(), Binary::from_owned(ob, env)).encode(env)) + } + Err(e) => { + Ok((atoms::error(), e).encode(env)) + } + } +} fn build_tx_hashfilter<'a>(env: Env<'a>, signer: Binary<'a>, arg0: Binary<'a>, contract: Binary<'a>, function: Binary<'a>) -> Binary<'a> { let key = tx_filter::create_filter_key(&[&signer, &arg0, &contract, &function]); to_binary2(env, &key) } -#[rustler::nif] fn build_tx_hashfilters<'a>(env: Env<'a>, txus: Vec>) -> NifResult, Binary<'a>)>> { tx_filter::build_tx_hashfilters(env, txus) } -#[rustler::nif] -fn query_tx_hashfilter<'a>(env: Env<'a>, db: ResourceArc, signer: Binary<'a>, arg0: Binary<'a>, contract: Binary<'a>, function: Binary<'a>, - limit: u32, sort: bool, cursor: Option>) -> NifResult<(Option>, Vec>)> { - tx_filter::query_tx_hashfilter(env, &db.db, &signer, &arg0, &contract, &function, limit as usize, sort, cursor.map(|b| b.as_slice())) -} - rustler::init!("Elixir.RDB", load = on_load); diff --git a/ex/native/rdb/src/mpt.rs b/ex/native/rdb/src/mpt.rs new file mode 100644 index 0000000..6b74cb2 --- /dev/null +++ b/ex/native/rdb/src/mpt.rs @@ -0,0 +1,188 @@ +use hex::{decode, encode}; +use keccak_hasher::KeccakHasher; +use primitive_types::H256; +use rlp::{Rlp, RlpStream}; +use trie_db::Hasher; + +/// RLP encodes a number (similar to TypeScript's RLP.encode(number)) +pub fn rlp_encode_number(n: u64) -> Vec { + if n == 0 { + // RLP encoding of 0 is 0x80 + vec![0x80] + } else { + let mut stream = RlpStream::new(); + stream.append(&n); + stream.out().to_vec() + } +} + +/// Convert bytes to nibbles (hex characters) +fn bytes_to_nibbles(bytes: &[u8]) -> Vec { + let mut nibbles = Vec::with_capacity(bytes.len() * 2); + for byte in bytes { + nibbles.push(byte >> 4); + nibbles.push(byte & 0x0f); + } + nibbles +} + +/// Decode hex-prefix encoding used in Ethereum MPT +/// Returns (nibbles, is_leaf) +fn decode_hex_prefix(encoded: &[u8]) -> (Vec, bool) { + let Some(&first) = encoded.first() else { + return (vec![], false); + }; + let is_leaf = (first >> 4) >= 2; + let is_odd = (first >> 4) % 2 == 1; + + let mut nibbles = Vec::new(); + if is_odd { + nibbles.push(first & 0x0f); + } + + for byte in &encoded[1..] { + nibbles.push(byte >> 4); + nibbles.push(byte & 0x0f); + } + + (nibbles, is_leaf) +} + +/// Verifies an Ethereum Merkle Patricia Trie proof +/// +/// # Arguments +/// * `root` - The root hash of the trie (32 bytes) +/// * `key` - The RLP-encoded key to look up +/// * `proof` - Array of proof nodes (RLP-encoded) +/// +/// # Returns +/// * `Ok(value)` - Proof is valid and value was found +/// * `Err(_)` - Proof is invalid +pub fn verify_proof<'a>( + root: &'a [u8], + key: &'a [u8], + proof: impl Iterator, +) -> Result, String> { + if root.len() != 32 { + return Err(format!("Root must be 32 bytes, got {}", root.len())); + } + + let root_hash = H256::from_slice(root); + + // Convert key to nibbles + let key_nibbles = bytes_to_nibbles(key); + let mut key_index = 0; + + // Expected hash starts with root + let mut expected_hash = root_hash.as_bytes().to_vec(); + + for (i, node) in proof.enumerate() { + // Verify node hash matches expected + let node_hash = KeccakHasher::hash(node); + if expected_hash != node_hash { + return Err(format!( + "Hash mismatch at node {}: expected 0x{}, got 0x{}", + i, + encode(&expected_hash), + encode(node_hash.as_ref()) + )); + } + + let rlp = Rlp::new(node); + let item_count = rlp.item_count().map_err(|e| format!("RLP decode error at node {}: {}", i, e))?; + const LEAF_NODE_ITEM_COUNT: usize = 2; + const BRANCH_NODE_ITEM_COUNT: usize = 17; + + match item_count { + LEAF_NODE_ITEM_COUNT => { + // Extension or Leaf node + let path: Vec = rlp.at(0) + .map_err(|e| format!("Failed to get path at node {}: {}", i, e))? + .as_val() + .map_err(|e| format!("Failed to decode path at node {}: {}", i, e))?; + let (nibbles, is_leaf) = decode_hex_prefix(&path); + + // Verify path matches our key + for nibble in &nibbles { + match key_nibbles.get(key_index) { + Some(expected_nibble) if nibble == expected_nibble => key_index += 1, + Some(expected_nibble) => { + return Err(format!( + "Path mismatch at node {}, nibble {}: expected {}, got {}", + i, key_index, expected_nibble, nibble + )); + } + None => return Err(format!("Key too short for path at node {}", i)), + } + } + + if is_leaf { + // Leaf node - we should be at the end of our key + if key_index != key_nibbles.len() { + return Err(format!( + "Key not fully consumed at leaf node: {} remaining nibbles", + key_nibbles.len() - key_index + )); + } + // Return the value + let value: Vec = rlp.at(1) + .map_err(|e| format!("Failed to get value at node {}: {}", i, e))? + .as_val() + .map_err(|e| format!("Failed to decode value at node {}: {}", i, e))?; + return Ok(value); + } else { + // Extension node - follow the next hash + let next = rlp.at(1) + .map_err(|e| format!("Failed to get next at node {}: {}", i, e))?; + if next.is_data() && next.size() == 32 { + expected_hash = next.as_val() + .map_err(|e| format!("Failed to decode next hash at node {}: {}", i, e))?; + } else if next.is_list() { + // Inline node + expected_hash = next.as_raw().to_vec(); + } else { + expected_hash = next.as_val() + .map_err(|e| format!("Failed to decode next at node {}: {}", i, e))?; + } + } + } + BRANCH_NODE_ITEM_COUNT => { + // Branch node + let Some(&nibble) = key_nibbles.get(key_index) else { + // We've consumed all key nibbles, value is in position 16 + let value_rlp = rlp.at(16) + .map_err(|e| format!("Failed to get value at branch node {}: {}", i, e))?; + if value_rlp.is_empty() { + return Err(format!("No value at branch node terminus at node {}", i)); + } + let value: Vec = value_rlp.as_val() + .map_err(|e| format!("Failed to decode value at branch node {}: {}", i, e))?; + return Ok(value); + }; + key_index += 1; + + let next = rlp.at(nibble as usize) + .map_err(|e| format!("Failed to get branch at nibble {} in node {}: {}", nibble, i, e))?; + if next.is_empty() { + return Err(format!("Empty branch at nibble {} in node {}", nibble, i)); + } + + if next.is_data() && next.size() == 32 { + expected_hash = next.as_val() + .map_err(|e| format!("Failed to decode branch hash at node {}: {}", i, e))?; + } else if next.is_list() || (next.is_data() && next.size() < 32) { + // Inline node - the data itself is the next node + expected_hash = next.as_raw().to_vec(); + } else { + expected_hash = next.as_val() + .map_err(|e| format!("Failed to decode branch at node {}: {}", i, e))?; + } + } + _ => { + return Err(format!("Invalid node with {} items at position {}", item_count, i)); + } + } + } + + Err("Proof ended without finding value".to_string()) +}