diff --git a/.gitmodules b/.gitmodules index 1dc051b26..6d908e898 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,9 @@ [submodule "onchain/rollups/lib/forge-std"] path = onchain/rollups/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "onchain/permissionless-arbitration/lib/forge-std"] + path = onchain/permissionless-arbitration/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "onchain/permissionless-arbitration/lib/machine-solidity-step"] + path = onchain/permissionless-arbitration/lib/machine-solidity-step + url = https://github.com/cartesi/machine-solidity-step diff --git a/onchain/permissionless-arbitration/.dockerignore b/onchain/permissionless-arbitration/.dockerignore new file mode 100644 index 000000000..8c4ed6c48 --- /dev/null +++ b/onchain/permissionless-arbitration/.dockerignore @@ -0,0 +1,2 @@ +offchain/program/simple-linux-program/ +offchain/program/simple-program/ diff --git a/onchain/permissionless-arbitration/.gitignore b/onchain/permissionless-arbitration/.gitignore new file mode 100644 index 000000000..3f738c3f0 --- /dev/null +++ b/onchain/permissionless-arbitration/.gitignore @@ -0,0 +1,21 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +# Coverage +report +lcov.info + +# Test +test/uarch-log diff --git a/onchain/permissionless-arbitration/Dockerfile b/onchain/permissionless-arbitration/Dockerfile new file mode 100644 index 000000000..18487c523 --- /dev/null +++ b/onchain/permissionless-arbitration/Dockerfile @@ -0,0 +1,17 @@ +FROM cartesi/machine-emulator:main + +USER 0 +RUN apt-get -y update; apt-get -y install curl git; apt-get install -y procps +RUN curl -sSL https://github.com/foundry-rs/foundry/releases/download/nightly/foundry_nightly_linux_$(dpkg --print-architecture).tar.gz | \ + tar -zx -C /usr/local/bin + +ADD foundry.toml ./project/ +ADD lib ./project/lib/ +ADD src ./project/src/ +WORKDIR "./project" +RUN forge --version +RUN forge build + +ADD ./offchain/ ./offchain/ +RUN chmod +x ./offchain/entrypoint.lua +ENTRYPOINT ["./offchain/entrypoint.lua"] diff --git a/onchain/permissionless-arbitration/README.md b/onchain/permissionless-arbitration/README.md new file mode 100644 index 000000000..dc8bdc48d --- /dev/null +++ b/onchain/permissionless-arbitration/README.md @@ -0,0 +1,7 @@ +# Permissionless Arbitration (NxN) Lua prototype Node + +## Run example + +``` +docker build -t nxn_playground:latest . && docker run --rm nxn_playground:latest +``` diff --git a/onchain/permissionless-arbitration/coverage.sh b/onchain/permissionless-arbitration/coverage.sh new file mode 100755 index 000000000..aac315d20 --- /dev/null +++ b/onchain/permissionless-arbitration/coverage.sh @@ -0,0 +1,3 @@ +#!/bin/bash +forge coverage --report lcov +genhtml -o report --branch-coverage lcov.info diff --git a/onchain/permissionless-arbitration/foundry.toml b/onchain/permissionless-arbitration/foundry.toml new file mode 100644 index 000000000..1496f241d --- /dev/null +++ b/onchain/permissionless-arbitration/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +remappings = [ + 'step/=lib/machine-solidity-step/', +] diff --git a/onchain/permissionless-arbitration/lib/forge-std b/onchain/permissionless-arbitration/lib/forge-std new file mode 160000 index 000000000..66bf4e2c9 --- /dev/null +++ b/onchain/permissionless-arbitration/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 66bf4e2c92cf507531599845e8d5a08cc2e3b5bb diff --git a/onchain/permissionless-arbitration/lib/machine-solidity-step b/onchain/permissionless-arbitration/lib/machine-solidity-step new file mode 160000 index 000000000..61f810ece --- /dev/null +++ b/onchain/permissionless-arbitration/lib/machine-solidity-step @@ -0,0 +1 @@ +Subproject commit 61f810ece4d9405cbabd7ff0018c18cf30fd43cb diff --git a/onchain/permissionless-arbitration/offchain/.luarc.json b/onchain/permissionless-arbitration/offchain/.luarc.json new file mode 100644 index 000000000..5f8d0ef78 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/.luarc.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + + "runtime.version": "Lua 5.4", + + "diagnostics": { + "enable": true + }, + + "workspace.library": { + "runtime/lua": true + } +} diff --git a/onchain/permissionless-arbitration/offchain/blockchain/constants.lua b/onchain/permissionless-arbitration/offchain/blockchain/constants.lua new file mode 100644 index 000000000..3949135c7 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/blockchain/constants.lua @@ -0,0 +1,90 @@ +-- contains default 40 accounts of anvil test node +local constants = { + endpoint = "http://127.0.0.1:8545", + addresses = { + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + "0x976EA74026E726554dB657fA54763abd0C3a0aa9", + "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955", + "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f", + "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720", + "0xBcd4042DE499D14e55001CcbB24a551F3b954096", + "0x71bE63f3384f5fb98995898A86B02Fb2426c5788", + "0xFABB0ac9d68B0B445fB7357272Ff202C5651694a", + "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec", + "0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097", + "0xcd3B766CCDd6AE721141F452C550Ca635964ce71", + "0x2546BcD3c84621e976D8185a91A922aE77ECEc30", + "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E", + "0xdD2FD4581271e230360230F9337D5c0430Bf44C0", + "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199", + "0x09DB0a93B389bEF724429898f539AEB7ac2Dd55f", + "0x02484cb50AAC86Eae85610D6f4Bf026f30f6627D", + "0x08135Da0A343E492FA2d4282F2AE34c6c5CC1BbE", + "0x5E661B79FE2D3F6cE70F5AAC07d8Cd9abb2743F1", + "0x61097BA76cD906d2ba4FD106E757f7Eb455fc295", + "0xDf37F81dAAD2b0327A0A50003740e1C935C70913", + "0x553BC17A05702530097c3677091C5BB47a3a7931", + "0x87BdCE72c06C21cd96219BD8521bDF1F42C78b5e", + "0x40Fc963A729c542424cD800349a7E4Ecc4896624", + "0x9DCCe783B6464611f38631e6C851bf441907c710", + "0x1BcB8e569EedAb4668e55145Cfeaf190902d3CF2", + "0x8263Fce86B1b78F95Ab4dae11907d8AF88f841e7", + "0xcF2d5b3cBb4D7bF04e3F7bFa8e27081B52191f91", + "0x86c53Eb85D0B7548fea5C4B4F82b4205C8f6Ac18", + "0x1aac82773CB722166D7dA0d5b0FA35B0307dD99D", + "0x2f4f06d218E426344CFE1A83D53dAd806994D325", + "0x1003ff39d25F2Ab16dBCc18EcE05a9B6154f65F4", + "0x9eAF5590f2c84912A08de97FA28d0529361Deb9E", + "0x11e8F3eA3C6FcF12EcfF2722d75CEFC539c51a1C", + "0x7D86687F980A56b832e9378952B738b614A99dc6", + }, + pks = { + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", + "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", + "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba", + "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e", + "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356", + "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97", + "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6", + "0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897", + "0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82", + "0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1", + "0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd", + "0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa", + "0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61", + "0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0", + "0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd", + "0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0", + "0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e", + "0xeaa861a9a01391ed3d587d8a5a84ca56ee277629a8b02c22093a419bf240e65d", + "0xc511b2aa70776d4ff1d376e8537903dae36896132c90b91d52c1dfbae267cd8b", + "0x224b7eb7449992aac96d631d9677f7bf5888245eef6d6eeda31e62d2f29a83e4", + "0x4624e0802698b9769f5bdb260a3777fbd4941ad2901f5966b854f953497eec1b", + "0x375ad145df13ed97f8ca8e27bb21ebf2a3819e9e0a06509a812db377e533def7", + "0x18743e59419b01d1d846d97ea070b5a3368a3e7f6f0242cf497e1baac6972427", + "0xe383b226df7c8282489889170b0f68f66af6459261f4833a781acd0804fafe7a", + "0xf3a6b71b94f5cd909fb2dbb287da47badaa6d8bcdc45d595e2884835d8749001", + "0x4e249d317253b9641e477aba8dd5d8f1f7cf5250a5acadd1229693e262720a19", + "0x233c86e887ac435d7f7dc64979d7758d69320906a0d340d2b6518b0fd20aa998", + "0x85a74ca11529e215137ccffd9c95b2c72c5fb0295c973eb21032e823329b3d2d", + "0xac8698a440d33b866b6ffe8775621ce1a4e6ebd04ab7980deb97b3d997fc64fb", + "0xf076539fbce50f0513c488f32bf81524d30ca7a29f400d68378cc5b1b17bc8f2", + "0x5544b8b2010dbdbef382d254802d856629156aba578f453a76af01b81a80104e", + "0x47003709a0a9a4431899d4e014c1fd01c5aad19e873172538a02370a119bae11", + "0x9644b39377553a920edc79a275f45fa5399cbcf030972f771d0bca8097f9aad3", + "0xcaa7b4a2d30d1d565716199f068f69ba5df586cf32ce396744858924fdf827f0", + "0xfc5a028670e1b6381ea876dd444d3faaee96cffae6db8d93ca6141130259247c", + "0x5b92c5fe82d4fabee0bc6d95b4b8a3f9680a0ed7801f631035528f32c9eb2ad5", + "0xb68ac4aa2137dd31fd0732436d8e59e959bb62b4db2e6107b15f594caf0f405f", + }, +} + +return constants diff --git a/onchain/permissionless-arbitration/offchain/blockchain/node.lua b/onchain/permissionless-arbitration/offchain/blockchain/node.lua new file mode 100644 index 000000000..7dee3044e --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/blockchain/node.lua @@ -0,0 +1,183 @@ +local default_account_number = 40 + +local function stop_blockchain(handle, pid) + print(string.format("Stopping blockchain with pid %d...", pid)) + os.execute(string.format("kill -15 %i", pid)) + handle:close() + print "Blockchain stopped" +end + +local function start_blockchain() + print(string.format("Starting blockchain with %d accounts...", default_account_number)) + + local cmd = string.format([[sh -c "echo $$ ; exec anvil --block-time 1 -a %d > anvil.log 2>&1"]], default_account_number) + + local reader = io.popen(cmd) + assert(reader, "`popen` returned nil reader") + + local pid = tonumber(reader:read()) + + local handle = { reader = reader, pid = pid } + setmetatable(handle, { + __gc = function(t) + stop_blockchain(t.reader, t.pid) + end + }) + + print(string.format("Blockchain running with pid %d", pid)) + return handle +end + +local function capture_blockchain_data() + local blockchain_data = require "blockchain.constants" + return { address = blockchain_data.addresses, pk = blockchain_data.pks }, blockchain_data.endpoint +end + + +local function deploy_contracts(endpoint, deployer, initial_hash) + -- + -- Deploy Single Level Factory + print "Deploying Single Level factory..." + + local cmd_sl = string.format( + [[sh -c "forge create SingleLevelTournamentFactory --rpc-url=%s --private-key=%s"]], + endpoint, deployer + ) + + local handle_sl = io.popen(cmd_sl) + assert(handle_sl, "`popen` returned nil handle") + + local _, _, sl_factory_address = handle_sl:read("*a"):find("Deployed to: (0x%x+)") + assert(sl_factory_address, "deployment failed, factory_address is nil") + print("Single Level factory deployed at: " .. sl_factory_address) + handle_sl:close() + + -- + -- Deploy top Factory + print "Deploying Top factory..." + + local cmd_top = string.format( + [[sh -c "forge create TopTournamentFactory --rpc-url=%s --private-key=%s"]], + endpoint, deployer + ) + + local handle_top = io.popen(cmd_top) + assert(handle_top, "`popen` returned nil handle") + + local _, _, top_factory_address = handle_top:read("*a"):find("Deployed to: (0x%x+)") + assert(top_factory_address, "deployment failed, factory_address is nil") + print("Top factory deployed at: " .. top_factory_address) + handle_top:close() + + -- + -- Deploy middle Factory + print "Deploying Middle factory..." + + local cmd_mid = string.format( + [[sh -c "forge create MiddleTournamentFactory --rpc-url=%s --private-key=%s"]], + endpoint, deployer + ) + + local handle_mid = io.popen(cmd_mid) + assert(handle_mid, "`popen` returned nil handle") + + local _, _, mid_factory_address = handle_mid:read("*a"):find("Deployed to: (0x%x+)") + assert(mid_factory_address, "deployment failed, factory_address is nil") + print("Middle factory deployed at: " .. mid_factory_address) + handle_mid:close() + + -- + -- Deploy bottom Factory + print "Deploying Bottom factory..." + + local cmd_bot = string.format( + [[sh -c "forge create BottomTournamentFactory --rpc-url=%s --private-key=%s"]], + endpoint, deployer + ) + + local handle_bot = io.popen(cmd_bot) + assert(handle_bot, "`popen` returned nil handle") + + local _, _, bot_factory_address = handle_bot:read("*a"):find("Deployed to: (0x%x+)") + assert(bot_factory_address, "deployment failed, factory_address is nil") + print("Bottom factory deployed at: " .. bot_factory_address) + handle_bot:close() + + + -- + -- Deploy Tournament Factory + print "Deploying Tournament factory..." + + local cmd_tournament = string.format( + [[sh -c "forge create TournamentFactory --rpc-url=%s --private-key=%s --constructor-args %s %s %s %s"]], + endpoint, deployer, sl_factory_address, top_factory_address, mid_factory_address, bot_factory_address + ) + + local handle_tournament = io.popen(cmd_tournament) + assert(handle_tournament, "`popen` returned nil handle") + + local _, _, tournament_factory_address = handle_tournament:read("*a"):find("Deployed to: (0x%x+)") + assert(tournament_factory_address, "deployment failed, factory_address is nil") + print("tournament factory deployed at: " .. tournament_factory_address) + handle_tournament:close() + + + -- + -- Instantiate Root Tournament + print "Instantiate root tournament contract..." + + local cmd_root = string.format( + [[cast send --private-key "%s" --rpc-url "%s" "%s" "instantiateTop(bytes32)" "%s"]], + deployer, endpoint, tournament_factory_address, initial_hash + ) + + local handle_root = io.popen(cmd_root) + assert(handle_root, "`popen` returned nil handle") + + local _, _, a = handle_root:read("*a"):find [["data":"0x000000000000000000000000(%x+)"]] + local address = "0x" .. a + assert(address, "deployment failed, address is nil") + print("Contract deployed at: " .. address) + handle_root:close() + + return address +end + +local Blockchain = {} +Blockchain.__index = Blockchain + +function Blockchain:new() + local blockchain = {} + + local handle = start_blockchain() + local accounts, endpoint = capture_blockchain_data() + + blockchain._handle = handle + blockchain._accounts = accounts + blockchain.endpoint = endpoint + + setmetatable(blockchain, self) + return blockchain +end + +function Blockchain:default_account() + local current_account = 1 + local accounts = self._accounts + assert(current_account <= #accounts.address, "no more accounts") + + local account = { + address = accounts.address[current_account], + pk = accounts.pk[current_account] + } + + return account +end + +function Blockchain:deploy_contract(initial_hash, deployer) + assert(initial_hash) + deployer = deployer or self:default_account() + local address = deploy_contracts(self.endpoint, deployer.pk, initial_hash) + return address, deployer +end + +return Blockchain diff --git a/onchain/permissionless-arbitration/offchain/blockchain/reader.lua b/onchain/permissionless-arbitration/offchain/blockchain/reader.lua new file mode 100644 index 000000000..3a257683a --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/blockchain/reader.lua @@ -0,0 +1,342 @@ +local Hash = require "cryptography.hash" +local eth_ebi = require "utils.eth_ebi" + +local function parse_topics(json) + local _, _, topics = json:find( + [==["topics":%[([^%]]*)%]]==] + ) + + local t = {} + for k, _ in string.gmatch(topics, [["(0x%x+)"]]) do + table.insert(t, k) + end + + return t +end + +local function parse_data(json, sig) + local _, _, data = json:find( + [==["data":"(0x%x+)"]==] + ) + + local decoded_data = eth_ebi.decode_event_data(sig, data) + return decoded_data +end + +local function parse_meta(json) + local _, _, block_hash = json:find( + [==["blockHash":"(0x%x+)"]==] + ) + + local _, _, block_number = json:find( + [==["blockNumber":"(0x%x+)"]==] + ) + + local _, _, log_index = json:find( + [==["logIndex":"(0x%x+)"]==] + ) + + local t = { + block_hash = block_hash, + block_number = tonumber(block_number), + log_index = tonumber(log_index), + } + + return t +end + + +local function parse_logs(logs, data_sig) + local ret = {} + for k, _ in string.gmatch(logs, [[{[^}]*}]]) do + local emited_topics = parse_topics(k) + local decoded_data = parse_data(k, data_sig) + local meta = parse_meta(k) + table.insert(ret, { emited_topics = emited_topics, decoded_data = decoded_data, meta = meta }) + end + + return ret +end + +local function join_tables(...) + local function join(ret, t, ...) + if not t then return ret end + + for k, v in ipairs(t) do + ret[k] = v + end + + return join(ret, ...) + end + + local ret = join({}, ...) + return ret +end + +local function sort_and_dedup(t) + table.sort(t, function(a, b) + local m1, m2 = a.meta, b.meta + + if m1.block_number < m2.block_number then + return true + elseif m1.block_number > m2.block_number then + return false + else + if m1.log_index <= m2.log_index then + return true + else + return false + end + end + end) + + local ret = {} + for k, v in ipairs(t) do + local v2 = t[k + 1] + if not v2 then + table.insert(ret, v) + else + local m1, m2 = v.meta, v2.meta + if not (m1.block_number == m2.block_number and m1.log_index == m2.log_index) then + table.insert(ret, v) + end + end + end + + return ret +end + +local Reader = {} +Reader.__index = Reader + +function Reader:new() + local blockchain_data = require "blockchain.constants" + + local reader = { + endpoint = blockchain_data.endpoint + } + + setmetatable(reader, self) + return reader +end + +local cast_logs_template = [==[ +cast rpc -r "%s" eth_getLogs \ + '[{"fromBlock": "earliest", "toBlock": "latest", "address": "%s", "topics": [%s]}]' -w 2>&1 +]==] + +function Reader:_read_logs(tournament_address, sig, topics, data_sig) + topics = topics or { false, false, false } + local encoded_sig = eth_ebi.encode_sig(sig) + table.insert(topics, 1, encoded_sig) + assert(#topics == 4, "topics doesn't have four elements") + + local topics_strs = {} + for _, v in ipairs(topics) do + local s + if v then + s = '"' .. v .. '"' + else + s = "null" + end + table.insert(topics_strs, s) + end + local topic_str = table.concat(topics_strs, ", ") + + local cmd = string.format( + cast_logs_template, + self.endpoint, + tournament_address, + topic_str + ) + + local handle = io.popen(cmd) + assert(handle) + local logs = handle:read "*a" + handle:close() + + if logs:find "Error" then + error(string.format("Read logs `%s` failed:\n%s", sig, logs)) + end + + local ret = parse_logs(logs, data_sig) + return ret +end + +local cast_call_template = [==[ +cast call --rpc-url "%s" "%s" "%s" %s 2>&1 +]==] + +function Reader:_call(address, sig, args) + local quoted_args = {} + for _, v in ipairs(args) do + table.insert(quoted_args, '"' .. v .. '"') + end + local args_str = table.concat(quoted_args, " ") + + local cmd = string.format( + cast_call_template, + self.endpoint, + address, + sig, + args_str + ) + + local handle = io.popen(cmd) + assert(handle) + + local ret = {} + local str = handle:read() + while str do + if str:find "Error" or str:find "error" then + local err_str = handle:read "*a" + handle:close() + error(string.format("Call `%s` failed:\n%s%s", sig, str, err_str)) + end + + table.insert(ret, str) + str = handle:read() + end + handle:close() + + return ret +end + +function Reader:read_match_created(tournament_address, commitment_hash) + local sig = "matchCreated(bytes32,bytes32,bytes32)" + local data_sig = "(bytes32)" + + local logs = self:_read_logs(tournament_address, sig, { false, false, false }, data_sig) + + local ret = {} + for k, v in ipairs(logs) do + local log = {} + log.tournament_address = tournament_address + log.meta = v.meta + + log.commitment_one = Hash:from_digest_hex(v.emited_topics[2]) + log.commitment_two = Hash:from_digest_hex(v.emited_topics[3]) + log.left_hash = Hash:from_digest_hex(v.decoded_data[1]) + log.match_id_hash = log.commitment_one:join(log.commitment_two) + + ret[k] = log + end + + return ret +end + +function Reader:read_commitment_joined(tournament_address) + local sig = "commitmentJoined(bytes32)" + local data_sig = "(bytes32)" + + local logs = self:_read_logs(tournament_address, sig, { false, false, false }, data_sig) + + local ret = {} + for k, v in ipairs(logs) do + local log = {} + log.tournament_address = tournament_address + log.meta = v.meta + log.root = Hash:from_digest_hex(v.decoded_data[1]) + + ret[k] = log + end + + return ret +end + +function Reader:read_commitment(tournament_address, commitment_hash) + local sig = "getCommitment(bytes32)((uint64,uint64),bytes32)" + + local call_ret = self:_call(tournament_address, sig, { commitment_hash:hex_string() }) + assert(#call_ret == 2) + + local allowance, last_resume = call_ret[1]:match "%((%d+),(%d+)%)" + assert(allowance) + assert(last_resume) + local clock = { + allowance = tonumber(allowance), + last_resume = tonumber(last_resume) + } + + local ret = { + clock = clock, + final_state = Hash:from_digest_hex(call_ret[2]) + } + + return ret +end + +function Reader:read_tournament_created(tournament_address, match_id_hash) + local sig = "newInnerTournament(bytes32,address)" + local data_sig = "(address)" + + local logs = self:_read_logs(tournament_address, sig, { match_id_hash:hex_string(), false, false }, data_sig) + assert(#logs <= 1) + + if #logs == 0 then return false end + local log = logs[1] + + local ret = { + parent_match = match_id_hash, + new_tournament = log.decoded_data[1], + } + + return ret +end + +function Reader:match(address, match_id_hash) + local sig = "getMatch(bytes32)(bytes32,bytes32,bytes32,uint256,uint64,uint64)" + local ret = self:_call(address, sig, { match_id_hash:hex_string() }) + ret[1] = Hash:from_digest_hex(ret[1]) + ret[2] = Hash:from_digest_hex(ret[2]) + ret[3] = Hash:from_digest_hex(ret[3]) + + return ret +end + +function Reader:inner_tournament_winner(address) + local sig = "innerTournamentWinner()(bool,bytes32)" + local ret = self:_call(address, sig, {}) + ret[2] = Hash:from_digest_hex(ret[2]) + + return ret +end + +function Reader:root_tournament_winner(address) + local sig = "arbitrationResult()(bool,bytes32,bytes32)" + local ret = self:_call(address, sig, {}) + ret[2] = Hash:from_digest_hex(ret[2]) + ret[3] = Hash:from_digest_hex(ret[3]) + + return ret +end + +function Reader:maximum_delay(address) + local sig = "maximumEnforceableDelay()(uint64)" + local ret = self:_call(address, sig, {}) + + return ret +end + +local cast_advance_template = [[ +cast rpc -r "%s" evm_increaseTime %d +]] + +function Reader:advance_time(seconds) + local cmd = string.format( + cast_advance_template, + self.endpoint, + seconds + ) + + local handle = io.popen(cmd) + assert(handle) + local ret = handle:read "*a" + handle:close() + + if ret:find "Error" then + error(string.format("Advance time `%d`s failed:\n%s", seconds, ret)) + end +end + +return Reader diff --git a/onchain/permissionless-arbitration/offchain/blockchain/sender.lua b/onchain/permissionless-arbitration/offchain/blockchain/sender.lua new file mode 100644 index 000000000..305459ff1 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/blockchain/sender.lua @@ -0,0 +1,161 @@ +local Hash = require "cryptography.hash" +local MerkleTree = require "cryptography.merkle_tree" + +local function quote_args(args, not_quote) + local quoted_args = {} + for _, v in ipairs(args) do + if type(v) == "table" and (getmetatable(v) == Hash or getmetatable(v) == MerkleTree) then + if not_quote then + table.insert(quoted_args, v:hex_string()) + else + table.insert(quoted_args, '"' .. v:hex_string() .. '"') + end + elseif type(v) == "table" then + if v._tag == "tuple" then + local qa = quote_args(v, true) + local ca = table.concat(qa, ",") + local sb = "'(" .. ca .. ")'" + table.insert(quoted_args, sb) + else + local qa = quote_args(v, true) + local ca = table.concat(qa, ",") + local sb = "'[" .. ca .. "]'" + table.insert(quoted_args, sb) + end + elseif not_quote then + table.insert(quoted_args, tostring(v)) + else + table.insert(quoted_args, '"' .. v .. '"') + end + end + + return quoted_args +end + + +local Sender = {} +Sender.__index = Sender + +function Sender:new(account_index) + local blockchain_data = require "blockchain.constants" + + local sender = { + endpoint = blockchain_data.endpoint, + pk = blockchain_data.pks[account_index], + index = account_index, + tx_count = 0 + } + + setmetatable(sender, self) + return sender +end + +local cast_send_template = [[ +cast send --private-key "%s" --rpc-url "%s" "%s" "%s" %s 2>&1 +]] + +function Sender:_send_tx(tournament_address, sig, args) + local quoted_args = quote_args(args) + local args_str = table.concat(quoted_args, " ") + + local cmd = string.format( + cast_send_template, + self.pk, + self.endpoint, + tournament_address, + sig, + args_str + ) + + local handle = io.popen(cmd) + assert(handle) + + local ret = handle:read "*a" + if ret:find "Error" then + handle:close() + error(string.format("Send transaction `%s` reverted:\n%s", sig, ret)) + end + + self.tx_count = self.tx_count + 1 + handle:close() +end + +function Sender:tx_join_tournament(tournament_address, final_state, proof, left_child, right_child) + local sig = [[joinTournament(bytes32,bytes32[],bytes32,bytes32)]] + return pcall( + self._send_tx, + self, + tournament_address, + sig, + { final_state, proof, left_child, right_child } + ) +end + +function Sender:tx_advance_match( + tournament_address, commitment_one, commitment_two, left, right, new_left, new_right +) + local sig = [[advanceMatch((bytes32,bytes32),bytes32,bytes32,bytes32,bytes32)]] + return pcall( + self._send_tx, + self, + tournament_address, + sig, + { { commitment_one, commitment_two, _tag = "tuple" }, left, right, new_left, new_right } + ) +end + +function Sender:tx_seal_inner_match( + tournament_address, commitment_one, commitment_two, left, right, initial_hash, proof +) + local sig = + [[sealInnerMatchAndCreateInnerTournament((bytes32,bytes32),bytes32,bytes32,bytes32,bytes32[])]] + return pcall( + self._send_tx, + self, + tournament_address, + sig, + { { commitment_one, commitment_two, _tag = "tuple" }, left, right, initial_hash:hex_string(), proof } + ) +end + +function Sender:tx_win_inner_match(tournament_address, child_tournament_address, left, right) + local sig = + [[winInnerMatch(address,bytes32,bytes32)]] + return pcall( + self._send_tx, + self, + tournament_address, + sig, + { child_tournament_address, left, right } + ) +end + +function Sender:tx_seal_leaf_match( + tournament_address, commitment_one, commitment_two, left, right, initial_hash, proof +) + local sig = + [[sealLeafMatch((bytes32,bytes32),bytes32,bytes32,bytes32,bytes32[])]] + return pcall( + self._send_tx, + self, + tournament_address, + sig, + { { commitment_one, commitment_two, _tag = "tuple" }, left, right, initial_hash, proof } + ) +end + +function Sender:tx_win_leaf_match( + tournament_address, commitment_one, commitment_two, left, right, proof +) + local sig = + [[winLeafMatch((bytes32,bytes32),bytes32,bytes32,bytes)]] + return pcall( + self._send_tx, + self, + tournament_address, + sig, + { { commitment_one, commitment_two, _tag = "tuple" }, left, right, proof } + ) +end + +return Sender diff --git a/onchain/permissionless-arbitration/offchain/blockchain/utils.lua b/onchain/permissionless-arbitration/offchain/blockchain/utils.lua new file mode 100644 index 000000000..0d6fa0ae4 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/blockchain/utils.lua @@ -0,0 +1,22 @@ +local cast_advance_template = [[ +cast rpc -r "%s" evm_increaseTime %d +]] + +function advance_time(seconds, endpoint) + local cmd = string.format( + cast_advance_template, + endpoint, + seconds + ) + + local handle = io.popen(cmd) + assert(handle) + local ret = handle:read "*a" + handle:close() + + if ret:find "Error" then + error(string.format("Advance time `%d`s failed:\n%s", seconds, ret)) + end +end + +return { advance_time = advance_time } diff --git a/onchain/permissionless-arbitration/offchain/computation/commitment.lua b/onchain/permissionless-arbitration/offchain/computation/commitment.lua new file mode 100644 index 000000000..37a51049f --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/computation/commitment.lua @@ -0,0 +1,187 @@ +local MerkleBuilder = require "cryptography.merkle_builder" +local Machine = require "computation.machine" + +local arithmetic = require "utils.arithmetic" +local consts = require "constants" + +local ulte = arithmetic.ulte + +local function run_uarch_span(machine) + assert(machine.ucycle == 0) + machine:increment_uarch() + local builder = MerkleBuilder:new() + + local i = 0 + repeat + builder:add(machine:state().root_hash) + machine:increment_uarch() + i = i + 1 + until machine:state().uhalted + + -- Add all remaining fixed-point states, filling the tree up to the last leaf. + builder:add(machine:state().root_hash, consts.uarch_span - i) + + -- At this point, we've added `2^a - 1` hashes to the inner merkle builder. + -- Note that these states range from "meta" ucycle `1` to `2^a - 1`. + + -- Now we do the last state transition (ureset), and add the last state, + -- closing in a power-of-two number of leaves (`2^a` leaves). + machine:ureset() + builder:add(machine:state().root_hash) + + return builder:build() +end + +local function build_small_machine_commitment(base_cycle, log2_stride_count, machine) + machine:run(base_cycle) + local initial_state = machine:state().root_hash + + local builder = MerkleBuilder:new() + local instruction_count = arithmetic.max_uint(log2_stride_count - consts.log2_uarch_span) + local instruction = 0 + while ulte(instruction, instruction_count) do + builder:add(run_uarch_span(machine)) + instruction = instruction + 1 + + -- Optional optimization, just comment to remove. + if machine:state().halted then + builder:add(run_uarch_span(machine), instruction_count - instruction + 1) + break + end + end + + return initial_state, builder:build(initial_state) +end + + + +local function build_big_machine_commitment(base_cycle, log2_stride, log2_stride_count, machine) + machine:run(base_cycle) + local initial_state = machine:state().root_hash + + local builder = MerkleBuilder:new() + local instruction_count = arithmetic.max_uint(log2_stride_count) + local instruction = 0 + while ulte(instruction, instruction_count) do + local cycle = ((instruction + 1) << (log2_stride - consts.log2_uarch_span)) + machine:run(base_cycle + cycle) + + if not machine:state().halted then + builder:add(machine:state().root_hash) + instruction = instruction + 1 + else + -- add this loop plus all remainings + builder:add(machine:state().root_hash, instruction_count - instruction + 1) + break + end + end + + return initial_state, builder:build(initial_state) +end + +local function build_commitment(base_cycle, log2_stride, log2_stride_count, machine_path) + local machine = Machine:new_from_path(machine_path) + + if log2_stride >= consts.log2_uarch_span then + assert( + log2_stride + log2_stride_count <= + consts.log2_emulator_span + consts.log2_uarch_span + ) + return build_big_machine_commitment(base_cycle, log2_stride, log2_stride_count, machine) + else + assert(log2_stride == 0) + return build_small_machine_commitment(base_cycle, log2_stride_count, machine) + end +end + +local CommitmentBuilder = {} +CommitmentBuilder.__index = CommitmentBuilder + +function CommitmentBuilder:new(machine_path) + local c = { + machine_path = machine_path, + commitments = {} + } + setmetatable(c, self) + return c +end + +function CommitmentBuilder:build(base_cycle, level) + assert(level <= consts.levels) + if not self.commitments[level] then + self.commitments[level] = {} + elseif self.commitments[level][base_cycle] then + return self.commitments[level][base_cycle] + end + + local l = consts.levels - level + 1 + local log2_stride, log2_stride_count = consts.log2step[l], consts.heights[l] + + local _, commitment = build_commitment(base_cycle, log2_stride, log2_stride_count, self.machine_path) + self.commitments[level][base_cycle] = commitment + return commitment +end + +-- local path = "program/simple-program" +-- -- local initial, tree = build_commitment(0, 0, 64, path) +-- local initial, tree = build_commitment(400, 0, 67, path) +-- local initial, tree = build_commitment(0, 64, 63, path) +-- print(initial, tree.root_hash) + +-- 0x95ebed36f6708365e01abbec609b89e5b2909b7a127636886afeeffafaf0c2ec +-- 0x0f42278e1dd53a54a4743633bcbc3db7035fd9952eccf5fcad497b6f73c8917c +-- +--0xd4a3511d1c56eb421e64dc218e8d7bf29c5d3ad848306f04c1b7f43b8883b670 +--0x66af9174ab9acb9d47d036b2e735cb9ba31226fd9b06198ce5bc0782c5ca03ff +-- +-- 0x95ebed36f6708365e01abbec609b89e5b2909b7a127636886afeeffafaf0c2ec +-- 0xa27e413a85c252c5664624e5a53c5415148b443983d7101bb3ca88829d1ab269 + + +--[[ +--[[ +a = 2 +b = 2 + +states = 2^b + 1 + +x (0 0 0 | x) (0 0 0 | x) (0 0 0 | x) (0 0 0 | x) +0 1 2 3 0 1 2 3 0 1 +--]] + + + + +-- local function x(log2_stride, log2_stride_count, machine) +-- local uarch_instruction_count = arithmetic.max_uint(log2_stride_count) +-- local stride = 1 << log2_stride +-- local inner_builder = MerkleBuilder:new() + +-- local ucycle = stride +-- while ulte(ucycle, uarch_instruction_count) do +-- machine:run_uarch(ucycle) +-- local state = machine:state() + +-- if not state.uhalted then +-- inner_builder:add(state.state) +-- ucycle = ucycle + stride +-- else +-- -- add this loop plus all remainings +-- inner_builder:add(state.state, uarch_instruction_count - ucycle + 1) +-- ucycle = uarch_instruction_count +-- break +-- end +-- end + +-- -- At this point, we've added `uarch_instruction_count - 1` hashes to the inner merkle builder. +-- -- Now we do the last state transition (ureset), and add the last state, +-- -- closing in a power-of-two number of leaves (`2^a` leaves). +-- machine:ureset() +-- local state = machine:state() +-- inner_builder:add(state.state) + +-- return inner_builder:build() +-- end +--]] + +return CommitmentBuilder diff --git a/onchain/permissionless-arbitration/offchain/computation/fake_commitment.lua b/onchain/permissionless-arbitration/offchain/computation/fake_commitment.lua new file mode 100644 index 000000000..e724cfb0a --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/computation/fake_commitment.lua @@ -0,0 +1,26 @@ +local MerkleBuilder = require "cryptography.merkle_builder" +local Hash = require "cryptography.hash" +local consts = require "constants" + +local CommitmentBuilder = {} +CommitmentBuilder.__index = CommitmentBuilder + +function CommitmentBuilder:new(initial_hash, second_state) + local c = { initial_hash = initial_hash, second_state = second_state } + setmetatable(c, self) + return c +end + +function CommitmentBuilder:build(_, level) + local builder = MerkleBuilder:new() + if consts.log2step[consts.levels - level + 1] == 0 and self.second_state then + builder:add(self.second_state) + builder:add(Hash.zero, (1 << consts.heights[consts.levels - level + 1]) - 1) + else + builder:add(Hash.zero, 1 << consts.heights[consts.levels - level + 1]) + end + -- local commitment = Hash.zero:iterated_merkle(consts.heights[level]) + return builder:build(self.initial_hash) +end + +return CommitmentBuilder diff --git a/onchain/permissionless-arbitration/offchain/computation/machine.lua b/onchain/permissionless-arbitration/offchain/computation/machine.lua new file mode 100644 index 000000000..2bccfd115 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/computation/machine.lua @@ -0,0 +1,172 @@ +local Hash = require "cryptography.hash" +local arithmetic = require "utils.arithmetic" +local cartesi = require "cartesi" +local consts = require "constants" + +local ComputationState = {} +ComputationState.__index = ComputationState + +function ComputationState:new(root_hash, halted, uhalted) + local r = { + root_hash = root_hash, + halted = halted, + uhalted = uhalted + } + setmetatable(r, self) + return r +end + +function ComputationState:from_current_machine_state(machine) + local hash = Hash:from_digest(machine:get_root_hash()) + return ComputationState:new( + hash, + machine:read_iflags_H(), + machine:read_uarch_halt_flag() + ) +end + +ComputationState.__tostring = function(x) + return string.format( + "{root_hash = %s, halted = %s, uhalted = %s}", + x.root_hash, + x.halted, + x.uhalted + ) +end + + +-- +--- +-- + +local Machine = {} +Machine.__index = Machine + +function Machine:new_from_path(path) + local machine = cartesi.machine(path) + local start_cycle = machine:read_mcycle() + + -- Machine can never be advanced on the micro arch. + -- Validators must verify this first + assert(machine:read_uarch_cycle() == 0) + + local b = { + path = path, + machine = machine, + cycle = 0, + ucycle = 0, + start_cycle = start_cycle, + initial_hash = Hash:from_digest(machine:get_root_hash()) + } + + setmetatable(b, self) + return b +end + +function Machine:state() + return ComputationState:from_current_machine_state(self.machine) +end + +local function add_and_clamp(x, y) + if math.ult(x, arithmetic.max_uint64 - y) then + return x + y + else + return arithmetic.max_uint64 + end +end + +function Machine:run(cycle) + assert(arithmetic.ulte(self.cycle, cycle)) + local physical_cycle = add_and_clamp(self.start_cycle, cycle) -- TODO reconsider for lambda + + local machine = self.machine + while not (machine:read_iflags_H() or machine:read_mcycle() == physical_cycle) do + machine:run(physical_cycle) + end + + self.cycle = cycle +end + +function Machine:run_uarch(ucycle) + assert(arithmetic.ulte(self.ucycle, ucycle), string.format("%u, %u", self.ucycle, ucycle)) + self.machine:run_uarch(ucycle) + self.ucycle = ucycle +end + +function Machine:increment_uarch() + self.machine:run_uarch(self.ucycle + 1) + self.ucycle = self.ucycle + 1 +end + +function Machine:ureset() + self.machine:reset_uarch_state() + self.cycle = self.cycle + 1 + self.ucycle = 0 +end + +local keccak = require "cartesi".keccak + +local function hex_from_bin(bin) + assert(bin:len() == 32) + return "0x" .. (bin:gsub('.', function(c) + return string.format('%02x', string.byte(c)) + end)) +end + +local function ver(t, p, s) + local stride = p >> 3 + for k, v in ipairs(s) do + if (stride >> (k - 1)) % 2 == 0 then + t = keccak(t, v) + else + t = keccak(v, t) + end + end + + return t +end + + +function Machine:get_logs(path, cycle, ucycle) + local machine = Machine:new_from_path(path) + machine:run(cycle) + machine:run_uarch(ucycle) + + if ucycle == consts.uarch_span then + error "ureset, not implemented" + + machine:run_uarch(consts.uarch_span) + -- get reset-uarch logs + + return + end + + local logs = machine.machine:step_uarch { annotations = true, proofs = true } + + local encoded = {} + + for _, a in ipairs(logs.accesses) do + assert(a.log2_size == 3) + if a.type == "read" then + table.insert(encoded, a.read) + end + + table.insert(encoded, a.proof.target_hash) + + local siblings = arithmetic.array_reverse(a.proof.sibling_hashes) + for _, h in ipairs(siblings) do + table.insert(encoded, h) + end + + assert(ver(a.proof.target_hash, a.address, siblings) == a.proof.root_hash) + end + + local data = table.concat(encoded) + local hex_data = "0x" .. (data:gsub('.', function(c) + return string.format('%02x', string.byte(c)) + end)) + + return '"' .. hex_data .. '"' +end + +return Machine diff --git a/onchain/permissionless-arbitration/offchain/constants.lua b/onchain/permissionless-arbitration/offchain/constants.lua new file mode 100644 index 000000000..a4a6bd695 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/constants.lua @@ -0,0 +1,18 @@ +local arithmetic = require "utils.arithmetic" + +local log2_uarch_span = 16 +local log2_emulator_span = 47 + +local constants = { + levels = 3, + log2step = { 31, 16, 0 }, + heights = { 32, 15, 16 }, + + log2_uarch_span = log2_uarch_span, + uarch_span = arithmetic.max_uint(log2_uarch_span), + + log2_emulator_span = log2_emulator_span, + emulator_span = arithmetic.max_uint(log2_emulator_span), +} + +return constants diff --git a/onchain/permissionless-arbitration/offchain/cryptography/hash.lua b/onchain/permissionless-arbitration/offchain/cryptography/hash.lua new file mode 100644 index 000000000..74b4dd099 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/cryptography/hash.lua @@ -0,0 +1,106 @@ +local keccak = require "cartesi".keccak + +local function hex_from_bin(bin) + assert(bin:len() == 32) + return "0x" .. (bin:gsub('.', function(c) + return string.format('%02x', string.byte(c)) + end)) +end + +local function bin_from_hex(hex) + assert(hex:len() == 66, string.format("%s %d", hex, hex:len())) + local h = assert(hex:match("0x(%x+)"), hex) + return (h:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +local internalized_hahes = {} +local iterateds = {} + +local Hash = {} +Hash.__index = Hash + +function Hash:from_digest(digest) + assert(type(digest) == "string", digest:len() == 32) + + local x = internalized_hahes[digest] + if x then return x end + + local h = { digest = digest } + iterateds[h] = { h } + setmetatable(h, self) + internalized_hahes[digest] = h + return h +end + +function Hash:from_digest_hex(digest_hex) + assert(type(digest_hex) == "string", digest_hex:len() == 66) + local digest = bin_from_hex(digest_hex) + return self:from_digest(digest) +end + +function Hash:from_data(data) + local digest = keccak(data) + return self:from_digest(digest) +end + +function Hash:join(other_hash) + assert(Hash:is_of_type_hash(other_hash)) + + local digest = keccak(self.digest, other_hash.digest) + local ret = Hash:from_digest(digest) + ret.left = self + ret.right = other_hash + return ret +end + +function Hash:children() + local left, right = self.left, self.right + if left and right then + return true, left, right + else + return false + end +end + +function Hash:iterated_merkle(level) + level = level + 1 + local iterated = iterateds[self] + + local ret = iterated[level] + if ret then return ret end + + local i = #iterated -- at least 1 + local highest_level = iterated[i] + while i < level do + highest_level = highest_level:join(highest_level) + i = i + 1 + iterated[i] = highest_level + end + + return highest_level +end + +function Hash:hex_string() + return hex_from_bin(self.digest) +end + +Hash.__tostring = function(x) + return hex_from_bin(x.digest) +end + +local zero_bytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000" +local zero_hash = Hash:from_digest_hex(zero_bytes32) + +Hash.zero = zero_hash + +function Hash:is_zero() + return self == zero_hash +end + +function Hash:is_of_type_hash(x) + return getmetatable(x) == self +end + +return Hash diff --git a/onchain/permissionless-arbitration/offchain/cryptography/merkle_builder.lua b/onchain/permissionless-arbitration/offchain/cryptography/merkle_builder.lua new file mode 100644 index 000000000..b84ff60d6 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/cryptography/merkle_builder.lua @@ -0,0 +1,139 @@ +local MerkleTree = require "cryptography.merkle_tree" +local arithmetic = require "utils.arithmetic" + +local ulte = arithmetic.ulte +local semi_sum = arithmetic.semi_sum + +local Slice = {} +Slice.__index = Slice + +function Slice:new(arr, start_idx_inc, end_idx_ex) + start_idx_inc = start_idx_inc or 1 + end_idx_ex = end_idx_ex or (#arr + 1) + assert(start_idx_inc > 0) + assert(ulte(start_idx_inc, end_idx_ex)) + assert(end_idx_ex <= #arr + 1) + local s = { + arr = arr, + start_idx_inc = start_idx_inc, + end_idx_ex = end_idx_ex, + } + setmetatable(s, self) + return s +end + +function Slice:slice(si, ei) + assert(si > 0) + assert(ulte(si, ei)) + local start_idx_inc = self.start_idx_inc + si - 1 + local end_idx_ex = self.start_idx_inc + ei - 1 + assert(ulte(end_idx_ex, self.end_idx_ex)) + return Slice:new(self.arr, start_idx_inc, end_idx_ex) +end + +function Slice:len() + return self.end_idx_ex - self.start_idx_inc +end + +function Slice:get(idx) + idx = assert(math.tointeger(idx)) + assert(idx > 0) + local i = self.start_idx_inc + idx - 1 + assert(i < self.end_idx_ex) + return self.arr[i] +end + +function Slice:find_cell_containing(elem) + local l, r = 1, self:len() + + while math.ult(l, r) do + local m = semi_sum(l, r) + + -- `-1` on both sides changes semantics on underflow... zero means 2^64. + if math.ult(self:get(m).accumulated_count - 1, elem - 1) then + l = m + 1 + else + r = m + end + end + + return l +end + +local MerkleBuilder = {} +MerkleBuilder.__index = MerkleBuilder + +function MerkleBuilder:new() + local m = { + leafs = {}, + } + setmetatable(m, self) + return m +end + +function MerkleBuilder:add(hash, rep) + rep = rep or 1 + assert(math.ult(0, rep)) + + local last = self.leafs[#self.leafs] + if last then + assert(last.accumulated_count ~= 0, "merkle builder is full") + local accumulated_count = rep + last.accumulated_count + + if not math.ult(rep, accumulated_count) then -- overflow... + assert(accumulated_count == 0) -- then it has to be zero, and nothing else can fit. + end + + table.insert(self.leafs, { hash = hash, accumulated_count = accumulated_count }) + else + table.insert(self.leafs, { hash = hash, accumulated_count = rep }) + end +end + +local function merkle(leafs, log2size, stride) + local first_time = stride * (1 << log2size) + 1 + local last_time = (stride + 1) * (1 << log2size) + + local first_cell = leafs:find_cell_containing(first_time) + local last_cell = leafs:find_cell_containing(last_time) + + if first_cell == last_cell then + return leafs:get(first_cell).hash:iterated_merkle(log2size) + end + + local slice = leafs:slice(first_cell, last_cell + 1) + local hash_left = merkle(slice, log2size - 1, stride << 1) + local hash_right = merkle(slice, log2size - 1, (stride << 1) + 1) + + return hash_left:join(hash_right) +end + +function MerkleBuilder:build(implicit_hash) + local last = assert(self.leafs[#self.leafs], #self.leafs) + local count = last.accumulated_count + + local log2size + if count == 0 then + log2size = 64 + else + assert(arithmetic.is_pow2(count), count) + log2size = arithmetic.ctz(count) + end + + local root_hash = merkle(Slice:new(self.leafs), log2size, 0) + return MerkleTree:new(self.leafs, root_hash, log2size, implicit_hash) +end + +-- local Hash = require "cryptography.hash" +-- local builder = MerkleBuilder:new() +-- builder:add(Hash.zero, 2) +-- builder:add(Hash.zero) +-- builder:add(Hash.zero) +-- builder:add(Hash.zero, 3) +-- builder:add(Hash.zero) +-- builder:add(Hash.zero, 0 - 8) +-- print(builder:build().root_hash) + +-- print(Hash.zero:iterated_merkle(64)) + +return MerkleBuilder diff --git a/onchain/permissionless-arbitration/offchain/cryptography/merkle_tree.lua b/onchain/permissionless-arbitration/offchain/cryptography/merkle_tree.lua new file mode 100644 index 000000000..13673e618 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/cryptography/merkle_tree.lua @@ -0,0 +1,103 @@ +local arithmetic = require "utils.arithmetic" + +local MerkleTree = {} +MerkleTree.__index = MerkleTree + +function MerkleTree:new(leafs, root_hash, log2size, implicit_hash) + local m = { + leafs = leafs, + root_hash = root_hash, + digest_hex = root_hash.digest_hex, + log2size = log2size, + implicit_hash = implicit_hash + } + setmetatable(m, self) + return m +end + +function MerkleTree:join(other_hash) + return self.root_hash:join(other_hash) +end + +function MerkleTree:children() + return self.root_hash:children() +end + +function MerkleTree:iterated_merkle(level) + return self.root_hash:iterated_merkle(level) +end + +function MerkleTree:hex_string() + return self.root_hash:hex_string() +end + +MerkleTree.__tostring = function(x) + return x.root_hash:hex_string() +end + + +local function generate_proof(proof, root, height, include_index) + if height == 0 then + proof.leaf = root + return + end + + local new_height = height - 1 + local ok, left, right = root:children() + assert(ok) + + if (include_index >> new_height) & 1 == 0 then + generate_proof(proof, left, new_height, include_index) + table.insert(proof, right) + else + generate_proof(proof, right, new_height, include_index) + table.insert(proof, left) + end +end + +function MerkleTree:prove_leaf(index) + local height + local l = assert(self.leafs[1]) + if l.log2size then + height = l.log2size + self.log2size + else + height = self.log2size + end + + assert((index >> height) == 0) + local proof = {} + generate_proof(proof, self.root_hash, height, index) + return proof.leaf, proof +end + +function MerkleTree:last() + local proof = {} + local ok, left, right = self.root_hash:children() + local old_right = self.root_hash + + while ok do + table.insert(proof, left) + old_right = right + ok, left, right = right:children() + end + + return old_right, arithmetic.array_reverse(proof) +end + +-- local Hash = require "cryptography.hash" +-- local MerkleBuilder = require "cryptography.merkle_builder" +-- local builder = MerkleBuilder:new() +-- builder:add(Hash.zero, 1 << 8) +-- local mt = builder:build() + +-- local i, p = mt:last((1 << 8) - 1) +-- local r = assert(i) +-- print(i) +-- for _, v in ipairs(p) do +-- print(v) +-- r = v:join(r) +-- end + +-- print("FINAL", r, mt.root_hash) + +return MerkleTree diff --git a/onchain/permissionless-arbitration/offchain/entrypoint.lua b/onchain/permissionless-arbitration/offchain/entrypoint.lua new file mode 100755 index 000000000..68e88b21b --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/entrypoint.lua @@ -0,0 +1,99 @@ +#!/usr/bin/lua +package.path = package.path .. ";/opt/cartesi/lib/lua/5.4/?.lua" +package.path = package.path .. ";./offchain/?.lua" +package.cpath = package.cpath .. ";/opt/cartesi/lib/lua/5.4/?.so" + +local machine_path = "offchain/program/simple-program" +local FF_TIME = 30 +local IDLE_LIMIT = 5 +local INACTIVE_LIMIT = 10 + +local helper = require 'utils.helper' +local blockchain_utils = require "blockchain.utils" +local time = require "utils.time" +local blockchain_constants = require "blockchain.constants" +local Blockchain = require "blockchain.node" +local Machine = require "computation.machine" + +print "Hello, world!" +os.execute "cd offchain/program && ./gen_machine_simple.sh" + +local m = Machine:new_from_path(machine_path) +local initial_hash = m:state().root_hash +local blockchain = Blockchain:new() +time.sleep(2) +local contract = blockchain:deploy_contract(initial_hash) + +-- add more player instances here +local cmds = { + string.format([[sh -c "echo $$ ; exec ./offchain/player/honest_player.lua %d %s %s | tee honest.log"]], 1, contract, machine_path), + string.format([[sh -c "echo $$ ; exec ./offchain/player/dishonest_player.lua %d %s %s %s | tee dishonest.log"]], 2, contract, machine_path, initial_hash) +} +local pid_reader = {} +local pid_player = {} + +for i, cmd in ipairs(cmds) do + local reader = io.popen(cmd) + local pid = reader:read() + pid_reader[pid] = reader + pid_player[pid] = i +end + +-- gracefully end children processes +setmetatable(pid_reader, { + __gc = function(t) + helper.stop_players(t) + end +}) + +local no_active_players = 0 +local all_idle = 0 +local last_ts = [[01/01/2000 00:00:00]] +while true do + local players = 0 + + for pid, reader in pairs(pid_reader) do + local msg_out = 0 + players = players + 1 + last_ts, msg_out = helper.log_to_ts(reader, last_ts) + + -- close the reader and delete the reader entry when there's no more msg in the buffer + -- and the process has already ended + if msg_out == 0 and helper.is_zombie(pid) then + helper.log(pid_player[pid], string.format("player process %s is dead", pid)) + reader:close() + pid_reader[pid] = nil + pid_player[pid] = nil + end + end + + if players > 0 then + if helper.all_players_idle(pid_player) then + all_idle = all_idle + 1 + helper.rm_all_players_idle(pid_player) + else + all_idle = 0 + end + + -- if all players are idle for `IDLE_LIMIT` consecutive iterations, advance blockchain + if all_idle == IDLE_LIMIT then + print(string.format("all players idle, fastforward blockchain for %d seconds...", FF_TIME)) + blockchain_utils.advance_time(FF_TIME, blockchain_constants.endpoint) + all_idle = 0 + end + end + + if players == 0 then + no_active_players = no_active_players + 1 + else + no_active_players = 0 + end + + -- if no active player processes for `INACTIVE_LIMIT` consecutive iterations, break loop + if no_active_players == INACTIVE_LIMIT then + print("no active players, end program...") + break + end +end + +print "Good-bye, world!" diff --git a/onchain/permissionless-arbitration/offchain/player/dishonest_player.lua b/onchain/permissionless-arbitration/offchain/player/dishonest_player.lua new file mode 100755 index 000000000..219fb6b57 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/player/dishonest_player.lua @@ -0,0 +1,40 @@ +#!/usr/bin/lua +package.path = package.path .. ";/opt/cartesi/lib/lua/5.4/?.lua" +package.path = package.path .. ";./offchain/?.lua" +package.cpath = package.cpath .. ";/opt/cartesi/lib/lua/5.4/?.so" + +local State = require "player.state" +local Hash = require "cryptography.hash" +local Sender = require "blockchain.sender" +local HonestStrategy = require "player.honest_strategy" + +local time = require "utils.time" +local helper = require 'utils.helper' + +local player_index = tonumber(arg[1]) +local tournament = arg[2] +local machine_path = arg[3] +local initial_hash = Hash:from_digest_hex(arg[4]) + +local state = State:new(tournament) +local sender = Sender:new(player_index) +local honest_strategy +do + local FakeCommitmentBuilder = require "computation.fake_commitment" + local builder = FakeCommitmentBuilder:new(initial_hash) + honest_strategy = HonestStrategy:new(builder, machine_path, sender) +end + +while true do + state:fetch() + local tx_count = sender.tx_count + if honest_strategy:react(state) then break end + -- player is considered idle if no tx sent in current iteration + if tx_count == sender.tx_count then + helper.log(player_index, "player idling") + helper.touch_player_idle(player_index) + else + helper.rm_player_idle(player_index) + end + time.sleep(1) +end diff --git a/onchain/permissionless-arbitration/offchain/player/honest_player.lua b/onchain/permissionless-arbitration/offchain/player/honest_player.lua new file mode 100755 index 000000000..36673b8cd --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/player/honest_player.lua @@ -0,0 +1,39 @@ +#!/usr/bin/lua +package.path = package.path .. ";/opt/cartesi/lib/lua/5.4/?.lua" +package.path = package.path .. ";./offchain/?.lua" +package.cpath = package.cpath .. ";/opt/cartesi/lib/lua/5.4/?.so" + +local State = require "player.state" +local Hash = require "cryptography.hash" +local HonestStrategy = require "player.honest_strategy" +local Sender = require "blockchain.sender" + +local time = require "utils.time" +local helper = require 'utils.helper' + +local player_index = tonumber(arg[1]) +local tournament = arg[2] +local machine_path = arg[3] + +local state = State:new(tournament) +local sender = Sender:new(player_index) +local honest_strategy +do + local CommitmentBuilder = require "computation.commitment" + local builder = CommitmentBuilder:new(machine_path) + honest_strategy = HonestStrategy:new(builder, machine_path, sender) +end + +while true do + state:fetch() + local tx_count = sender.tx_count + if honest_strategy:react(state) then break end + -- player is considered idle if no tx sent in current iteration + if tx_count == sender.tx_count then + helper.log(player_index, "player idling") + helper.touch_player_idle(player_index) + else + helper.rm_player_idle(player_index) + end + time.sleep(1) +end diff --git a/onchain/permissionless-arbitration/offchain/player/honest_strategy.lua b/onchain/permissionless-arbitration/offchain/player/honest_strategy.lua new file mode 100644 index 000000000..73c984d18 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/player/honest_strategy.lua @@ -0,0 +1,258 @@ +local constants = require "constants" +local helper = require 'utils.helper' + +local Machine = require "computation.machine" + +local HonestStrategy = {} +HonestStrategy.__index = HonestStrategy + +function HonestStrategy:new(commitment_builder, machine_path, sender) + local honest_strategy = { + commitment_builder = commitment_builder, + machine_path = machine_path, + sender = sender + } + + setmetatable(honest_strategy, self) + return honest_strategy +end + +function HonestStrategy:_join_tournament(state, tournament, commitment) + local f, left, right = commitment:children(commitment.root_hash) + assert(f) + local last, proof = commitment:last() + + helper.log(self.sender.index, string.format( + "join tournament %s of level %d with commitment %s", + tournament.address, + tournament.level, + commitment.root_hash + )) + local ok, e = self.sender:tx_join_tournament( + tournament.address, + last, + proof, + left, + right + ) + if not ok then + helper.log(self.sender.index, string.format( + "join tournament reverted: %s", + e + )) + end +end + +function HonestStrategy:_react_match(state, match, commitment) + -- TODO call timeout if needed + + helper.log(self.sender.index, "Enter match at HEIGHT: " .. match.current_height) + if match.current_height == 0 then + -- match sealed + if match.tournament.level == 1 then + local f, left, right = commitment.root_hash:children() + assert(f) + + helper.log(self.sender.index, string.format( + "Calculating access logs for step %s", + match.running_leaf + )) + + local cycle = (match.running_leaf >> constants.log2_uarch_span):touinteger() + local ucycle = (match.running_leaf & constants.uarch_span):touinteger() + local logs = Machine:get_logs(self.machine_path, cycle, ucycle) + + helper.log(self.sender.index, string.format( + "win leaf match in tournament %s of level %d for commitment %s", + match.tournament.address, + match.tournament.level, + commitment.root_hash + )) + local ok, e = self.sender:tx_win_leaf_match( + match.tournament.address, + match.commitment_one, + match.commitment_two, + left, + right, + logs + ) + if not ok then + helper.log(self.sender.index, string.format( + "win leaf match reverted: %s", + e + )) + end + elseif match.inner_tournament then + return self:_react_tournament(state, match.inner_tournament) + end + elseif match.current_height == 1 then + -- match to be sealed + local found, left, right = match.current_other_parent:children() + if not found then + return + end + + local initial_hash, proof + if match.running_leaf:iszero() then + initial_hash, proof = commitment.implicit_hash, {} + else + initial_hash, proof = commitment:prove_leaf(match.running_leaf) + end + + if match.tournament.level == 1 then + helper.log(self.sender.index, string.format( + "seal leaf match in tournament %s of level %d for commitment %s", + match.tournament.address, + match.tournament.level, + commitment.root_hash + )) + local ok, e = self.sender:tx_seal_leaf_match( + match.tournament.address, + match.commitment_one, + match.commitment_two, + left, + right, + initial_hash, + proof + ) + if not ok then + helper.log(self.sender.index, string.format( + "seal leaf match reverted: %s", + e + )) + end + else + helper.log(self.sender.index, string.format( + "seal inner match in tournament %s of level %d for commitment %s", + match.tournament.address, + match.tournament.level, + commitment.root_hash + )) + local ok, e = self.sender:tx_seal_inner_match( + match.tournament.address, + match.commitment_one, + match.commitment_two, + left, + right, + initial_hash, + proof + ) + if not ok then + helper.log(self.sender.index, string.format( + "seal inner match reverted: %s", + e + )) + end + end + else + -- match running + local found, left, right = match.current_other_parent:children() + if not found then + return + end + + local new_left, new_right + if left ~= match.current_left then + local f + f, new_left, new_right = left:children() + assert(f) + else + local f + f, new_left, new_right = right:children() + assert(f) + end + + helper.log(self.sender.index, string.format( + "advance match with current height %d in tournament %s of level %d for commitment %s", + match.current_height, + match.tournament.address, + match.tournament.level, + commitment.root_hash + )) + local ok, e = self.sender:tx_advance_match( + match.tournament.address, + match.commitment_one, + match.commitment_two, + left, + right, + new_left, + new_right + ) + if not ok then + helper.log(self.sender.index, string.format( + "advance match reverted: %s", + e + )) + end + end +end + +function HonestStrategy:_react_tournament(state, tournament) + helper.log(self.sender.index, "Enter tournament at address: " .. tournament.address) + local commitment = self.commitment_builder:build( + tournament.base_big_cycle, + tournament.level + ) + + local tournament_winner = tournament.tournament_winner + if tournament_winner[1] == "true" then + if not tournament.parent then + helper.log(self.sender.index, "TOURNAMENT FINISHED, HURRAYYY") + helper.log(self.sender.index, "Winner commitment: " .. tournament_winner[2]:hex_string()) + helper.log(self.sender.index, "Final state: " .. tournament_winner[3]:hex_string()) + return true + else + local old_commitment = self.commitment_builder:build( + tournament.parent.base_big_cycle, + tournament.parent.level + ) + if tournament_winner[2] ~= old_commitment.root_hash then + helper.log(self.sender.index, "player lost tournament") + return true + end + + if tournament.commitments[commitment.root_hash].called_win then + helper.log(self.sender.index, "player already called winInnerMatch") + return + else + tournament.commitments[commitment.root_hash].called_win = true + end + + helper.log(self.sender.index, string.format( + "win tournament %s of level %d for commitment %s", + tournament.address, + tournament.level, + commitment.root_hash + )) + local _, left, right = old_commitment:children(old_commitment.root_hash) + local ok, e = self.sender:tx_win_inner_match( + tournament.parent.address, + tournament.address, + left, + right + ) + if not ok then + helper.log(self.sender.index, string.format( + "win inner match reverted: %s", + e + )) + end + return + end + end + + if not tournament.commitments[commitment.root_hash] then + self:_join_tournament(state, tournament, commitment) + else + local latest_match = tournament.commitments[commitment.root_hash].latest_match + if latest_match then + return self:_react_match(state, latest_match, commitment) + end + end +end + +function HonestStrategy:react(state) + return self:_react_tournament(state, state.root_tournament) +end + +return HonestStrategy diff --git a/onchain/permissionless-arbitration/offchain/player/state.lua b/onchain/permissionless-arbitration/offchain/player/state.lua new file mode 100644 index 000000000..7f009538e --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/player/state.lua @@ -0,0 +1,114 @@ +local constants = require "constants" +local bint = require 'utils.bint' (256) -- use 256 bits integers + +local Reader = require "blockchain.reader" + +local State = {} +State.__index = State + +function State:new(root_tournament_address) + local state = { + root_tournament = { + base_big_cycle = 0, + address = root_tournament_address, + level = constants.levels, + parent = false, + commitments = {}, + matches = {}, + tournament_winner = {} + }, + reader = Reader:new() + } + + setmetatable(state, self) + return state +end + +function State:fetch() + return self:_fetch_tournament(self.root_tournament) +end + +function State:_fetch_tournament(tournament) + local matches = self:_matches(tournament) + local commitments = self.reader:read_commitment_joined(tournament.address) + + for _, log in ipairs(commitments) do + local root = log.root + local status = self.reader:read_commitment(tournament.address, root) + tournament.commitments[root] = { status = status, latest_match = false } + end + + for _, match in ipairs(matches) do + if match then + self:_fetch_match(match) + tournament.commitments[match.commitment_one].latest_match = match + tournament.commitments[match.commitment_two].latest_match = match + end + end + tournament.matches = matches + + if not tournament.parent then + tournament.tournament_winner = self.reader:root_tournament_winner(tournament.address) + else + tournament.tournament_winner = self.reader:inner_tournament_winner(tournament.address) + end +end + +function State:_fetch_match(match) + if match.current_height == 0 then + -- match sealed + if match.tournament.level == 1 then + + match.finished = + self.reader:match(match.tournament.address, match.match_id_hash)[1]:is_zero() + + if match.finished then + match.delay = tonumber(self.reader:maximum_delay(match.tournament.address)[1]) + end + else + local address = self.reader:read_tournament_created( + match.tournament.address, + match.match_id_hash + ).new_tournament + + local new_tournament = {} + new_tournament.address = address + new_tournament.level = match.tournament.level - 1 + new_tournament.parent = match.tournament + new_tournament.base_big_cycle = match.base_big_cycle + new_tournament.commitments = {} + match.inner_tournament = new_tournament + + return self:_fetch_tournament(new_tournament) + end + end +end + +function State:_matches(tournament) + local matches = self.reader:read_match_created(tournament.address) + + for k, match in ipairs(matches) do + local m = self.reader:match(tournament.address, match.match_id_hash) + if m[1]:is_zero() and m[2]:is_zero() and m[3]:is_zero() then + matches[k] = false + else + match.current_other_parent = m[1] + match.current_left = m[2] + match.current_right = m[3] + match.running_leaf = bint(m[4]) + match.current_height = tonumber(m[5]) + match.level = tonumber(m[6]) + match.tournament = tournament + + local level = tournament.level + local base = bint(tournament.base_big_cycle) + local step = bint(1) << constants.log2step[level] + match.leaf_cycle = base + (step * match.running_leaf) + match.base_big_cycle = (match.leaf_cycle >> constants.log2_uarch_span):touinteger() + end + end + + return matches +end + +return State diff --git a/onchain/permissionless-arbitration/offchain/program/.gitignore b/onchain/permissionless-arbitration/offchain/program/.gitignore new file mode 100644 index 000000000..6e9e42527 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/program/.gitignore @@ -0,0 +1,2 @@ +simple-linux-program/ +simple-program/ diff --git a/onchain/permissionless-arbitration/offchain/program/README.md b/onchain/permissionless-arbitration/offchain/program/README.md new file mode 100644 index 000000000..4f74431a7 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/program/README.md @@ -0,0 +1,29 @@ +# RISC-V programs + +## Generate programs + +``` +cd program +``` + +``` +docker run --platform linux/amd64 -it --rm -h playground \ + -e USER=$(id -u -n) \ + -e GROUP=$(id -g -n) \ + -e UID=$(id -u) \ + -e GID=$(id -g) \ + -v (pwd):/home/$(id -u -n) \ + -w /home/$(id -u -n) \ + diegonehab/playground:develop /bin/bash -c "./gen_machine_linux.sh" +``` + +``` +docker run --platform linux/amd64 -it --rm -h playground \ + -e USER=$(id -u -n) \ + -e GROUP=$(id -g -n) \ + -e UID=$(id -u) \ + -e GID=$(id -g) \ + -v (pwd):/home/$(id -u -n) \ + -w /home/$(id -u -n) \ + diegonehab/playground:develop /bin/bash -c "./gen_machine_simple.sh" +``` diff --git a/onchain/permissionless-arbitration/offchain/program/bins/bootstrap.bin b/onchain/permissionless-arbitration/offchain/program/bins/bootstrap.bin new file mode 100755 index 000000000..82b19be5e Binary files /dev/null and b/onchain/permissionless-arbitration/offchain/program/bins/bootstrap.bin differ diff --git a/onchain/permissionless-arbitration/offchain/program/bins/rv64ui-p-addi.bin b/onchain/permissionless-arbitration/offchain/program/bins/rv64ui-p-addi.bin new file mode 100755 index 000000000..104034ad5 Binary files /dev/null and b/onchain/permissionless-arbitration/offchain/program/bins/rv64ui-p-addi.bin differ diff --git a/onchain/permissionless-arbitration/offchain/program/gen_machine_linux.sh b/onchain/permissionless-arbitration/offchain/program/gen_machine_linux.sh new file mode 100755 index 000000000..e64a96c2f --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/program/gen_machine_linux.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cartesi-machine \ + --max-mcycle=0 \ + --store="simple-linux-program" \ + -- echo "Hello, world!" diff --git a/onchain/permissionless-arbitration/offchain/program/gen_machine_simple.sh b/onchain/permissionless-arbitration/offchain/program/gen_machine_simple.sh new file mode 100755 index 000000000..4b33b89ab --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/program/gen_machine_simple.sh @@ -0,0 +1,9 @@ +#!/bin/bash +cartesi-machine \ + --no-root-flash-drive \ + --rom-image="./bins/bootstrap.bin" \ + --ram-image="./bins/rv64ui-p-addi.bin" \ + --uarch-ram-image="/usr/share/cartesi-machine/uarch/uarch-ram.bin" \ + --uarch-ram-length=0x1000000 \ + --max-mcycle=0 \ + --store="simple-program" diff --git a/onchain/permissionless-arbitration/offchain/utils/arithmetic.lua b/onchain/permissionless-arbitration/offchain/utils/arithmetic.lua new file mode 100644 index 000000000..1fe1688b4 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/utils/arithmetic.lua @@ -0,0 +1,69 @@ +local function max_uint(k) + assert(k <= 64) + return (1 << k) - 1 +end + +local max_uint64 = max_uint(64) + +local function ulte(x, y) + return x == y or math.ult(x, y) +end + +local function is_pow2(x) + return (x & (x - 1)) == 0 +end + +-- Returns number of leading zeroes of x. Shamelessly stolen from the book +-- Hacker's Delight. +local function clz(x) + if x == 0 then return 64 end + local n = 0 + if (x & 0xFFFFFFFF00000000) == 0 then + n = n + 32; x = x << 32 + end + if (x & 0xFFFF000000000000) == 0 then + n = n + 16; x = x << 16 + end + if (x & 0xFF00000000000000) == 0 then + n = n + 8; x = x << 8 + end + if (x & 0xF000000000000000) == 0 then + n = n + 4; x = x << 4 + end + if (x & 0xC000000000000000) == 0 then + n = n + 2; x = x << 2 + end + if (x & 0x8000000000000000) == 0 then n = n + 1 end + return n +end + +-- Returns number of trailing zeroes of x. Shamelessly stolen from the book +-- Hacker's Delight. +local function ctz(x) + x = x & (~x + 1) + return 63 - clz(x) +end + +local function semi_sum(a, b) + assert(ulte(a, b)) + return a + (b - a) // 2 +end + +local function array_reverse(x) + local n, m = #x, #x / 2 + for i = 1, m do + x[i], x[n - i + 1] = x[n - i + 1], x[i] + end + return x +end + +return { + max_uint = max_uint, + max_uint64 = max_uint64, + ulte = ulte, + is_pow2 = is_pow2, + clz = clz, + ctz = ctz, + semi_sum = semi_sum, + array_reverse = array_reverse, +} diff --git a/onchain/permissionless-arbitration/offchain/utils/bint.lua b/onchain/permissionless-arbitration/offchain/utils/bint.lua new file mode 100644 index 000000000..64b6df71e --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/utils/bint.lua @@ -0,0 +1,1756 @@ +--[[-- +lua-bint - v0.5.1 - 26/Jun/2023 +Eduardo Bart - edub4rt@gmail.com +https://github.com/edubart/lua-bint + +Small portable arbitrary-precision integer arithmetic library in pure Lua for +computing with large integers. + +Different from most arbitrary-precision integer libraries in pure Lua out there this one +uses an array of lua integers as underlying data-type in its implementation instead of +using strings or large tables, this make it efficient for working with fixed width integers +and to make bitwise operations. + +## Design goals + +The main design goal of this library is to be small, correct, self contained and use few +resources while retaining acceptable performance and feature completeness. + +The library is designed to follow recent Lua integer semantics, this means that +integer overflow warps around, +signed integers are implemented using two-complement arithmetic rules, +integer division operations rounds towards minus infinity, +any mixed operations with float numbers promotes the value to a float, +and the usual division/power operation always promotes to floats. + +The library is designed to be possible to work with only unsigned integer arithmetic +when using the proper methods. + +All the lua arithmetic operators (+, -, *, //, /, %) and bitwise operators (&, |, ~, <<, >>) +are implemented as metamethods. + +The integer size must be fixed in advance and the library is designed to be more efficient when +working with integers of sizes between 64-4096 bits. If you need to work with really huge numbers +without size restrictions then use another library. This choice has been made to have more efficiency +in that specific size range. + +## Usage + +First on you should require the bint file including how many bits the bint module will work with, +by calling the returned function from the require, for example: + +```lua +local bint = require 'bint'(1024) +``` + +For more information about its arguments see @{newmodule}. +Then when you need create a bint, you can use one of the following functions: + +* @{bint.fromuinteger} (convert from lua integers, but read as unsigned integer) +* @{bint.frominteger} (convert from lua integers, preserving the sign) +* @{bint.frombase} (convert from arbitrary bases, like hexadecimal) +* @{bint.fromstring} (convert from arbitrary string, support binary/hexadecimal/decimal) +* @{bint.trunc} (convert from lua numbers, truncating the fractional part) +* @{bint.new} (convert from anything, asserts on invalid integers) +* @{bint.tobint} (convert from anything, returns nil on invalid integers) +* @{bint.parse} (convert from anything, returns a lua number as fallback) +* @{bint.zero} +* @{bint.one} +* `bint` + +You can also call `bint` as it is an alias to `bint.new`. +In doubt use @{bint.new} to create a new bint. + +Then you can use all the usual lua numeric operations on it, +all the arithmetic metamethods are implemented. +When you are done computing and need to get the result, +get the output from one of the following functions: + +* @{bint.touinteger} (convert to a lua integer, wraps around as an unsigned integer) +* @{bint.tointeger} (convert to a lua integer, wraps around, preserves the sign) +* @{bint.tonumber} (convert to lua float, losing precision) +* @{bint.tobase} (convert to a string in any base) +* @{bint.__tostring} (convert to a string in base 10) + +To output a very large integer with no loss you probably want to use @{bint.tobase} +or call `tostring` to get a string representation. + +## Precautions + +All library functions can be mixed with lua numbers, +this makes easy to mix operations between bints and lua numbers, +however the user should take care in some situations: + +* Don't mix integers and float operations if you want to work with integers only. +* Don't use the regular equal operator ('==') to compare values from this library, +unless you know in advance that both values are of the same primitive type, +otherwise it will always return false, use @{bint.eq} to be safe. +* Don't pass fractional numbers to functions that an integer is expected +* Don't mix operations between bint classes with different sizes as this is not supported, this +will throw assertions. +* Remember that casting back to lua integers or numbers precision can be lost. +* For dividing while preserving integers use the @{bint.__idiv} (the '//' operator). +* For doing power operation preserving integers use the @{bint.ipow} function. +* Configure the proper integer size you intend to work with, otherwise large integers may wrap around. + +]] + +-- Returns number of bits of the internal lua integer type. +local function luainteger_bitsize() + local n, i = -1, 0 + repeat + n, i = n >> 16, i + 16 + until n == 0 + return i +end + +local math_type = math.type +local math_floor = math.floor +local math_abs = math.abs +local math_ceil = math.ceil +local math_modf = math.modf +local math_mininteger = math.mininteger +local math_maxinteger = math.maxinteger +local math_max = math.max +local math_min = math.min +local string_format = string.format +local table_insert = table.insert +local table_concat = table.concat +local table_unpack = table.unpack + +local memo = {} + +--- Create a new bint module representing integers of the desired bit size. +-- This is the returned function when `require 'bint'` is called. +-- @function newmodule +-- @param bits Number of bits for the integer representation, must be multiple of wordbits and +-- at least 64. +-- @param[opt] wordbits Number of the bits for the internal word, +-- defaults to half of Lua's integer size. +local function newmodule(bits, wordbits) + local intbits = luainteger_bitsize() + bits = bits or 256 + wordbits = wordbits or (intbits // 2) + + -- Memoize bint modules + local memoindex = bits * 64 + wordbits + if memo[memoindex] then + return memo[memoindex] + end + + -- Validate + assert(bits % wordbits == 0, 'bitsize is not multiple of word bitsize') + assert(2 * wordbits <= intbits, 'word bitsize must be half of the lua integer bitsize') + assert(bits >= 64, 'bitsize must be >= 64') + assert(wordbits >= 8, 'wordbits must be at least 8') + assert(bits % 8 == 0, 'bitsize must be multiple of 8') + + -- Create bint module + local bint = {} + bint.__index = bint + + --- Number of bits representing a bint instance. + bint.bits = bits + + -- Constants used internally + local BINT_BITS = bits + local BINT_BYTES = bits // 8 + local BINT_WORDBITS = wordbits + local BINT_SIZE = BINT_BITS // BINT_WORDBITS + local BINT_WORDMAX = (1 << BINT_WORDBITS) - 1 + local BINT_WORDMSB = (1 << (BINT_WORDBITS - 1)) + local BINT_LEPACKFMT = '<' .. ('I' .. (wordbits // 8)):rep(BINT_SIZE) + local BINT_MATHMININTEGER, BINT_MATHMAXINTEGER + local BINT_MININTEGER + + --- Create a new bint with 0 value. + function bint.zero() + local x = setmetatable({}, bint) + for i = 1, BINT_SIZE do + x[i] = 0 + end + return x + end + + local bint_zero = bint.zero + + --- Create a new bint with 1 value. + function bint.one() + local x = setmetatable({}, bint) + x[1] = 1 + for i = 2, BINT_SIZE do + x[i] = 0 + end + return x + end + + local bint_one = bint.one + + -- Convert a value to a lua integer without losing precision. + local function tointeger(x) + x = tonumber(x) + local ty = math_type(x) + if ty == 'float' then + local floorx = math_floor(x) + if floorx == x then + x = floorx + ty = math_type(x) + end + end + if ty == 'integer' then + return x + end + end + + --- Create a bint from an unsigned integer. + -- Treats signed integers as an unsigned integer. + -- @param x A value to initialize from convertible to a lua integer. + -- @return A new bint or nil in case the input cannot be represented by an integer. + -- @see bint.frominteger + function bint.fromuinteger(x) + x = tointeger(x) + if x then + if x == 1 then + return bint_one() + elseif x == 0 then + return bint_zero() + end + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x & BINT_WORDMAX + x = x >> BINT_WORDBITS + end + return n + end + end + + local bint_fromuinteger = bint.fromuinteger + + --- Create a bint from a signed integer. + -- @param x A value to initialize from convertible to a lua integer. + -- @return A new bint or nil in case the input cannot be represented by an integer. + -- @see bint.fromuinteger + function bint.frominteger(x) + x = tointeger(x) + if x then + if x == 1 then + return bint_one() + elseif x == 0 then + return bint_zero() + end + local neg = false + if x < 0 then + x = math_abs(x) + neg = true + end + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x & BINT_WORDMAX + x = x >> BINT_WORDBITS + end + if neg then + n:_unm() + end + return n + end + end + + local bint_frominteger = bint.frominteger + + local basesteps = {} + + -- Compute the read step for frombase function + local function getbasestep(base) + local step = basesteps[base] + if step then + return step + end + step = 0 + local dmax = 1 + local limit = math_maxinteger // base + repeat + step = step + 1 + dmax = dmax * base + until dmax >= limit + basesteps[base] = step + return step + end + + -- Compute power with lua integers. + local function ipow(y, x, n) + if n == 1 then + return y * x + elseif n & 1 == 0 then --even + return ipow(y, x * x, n // 2) + end + return ipow(x * y, x * x, (n - 1) // 2) + end + + --- Create a bint from a string of the desired base. + -- @param s The string to be converted from, + -- must have only alphanumeric and '+-' characters. + -- @param[opt] base Base that the number is represented, defaults to 10. + -- Must be at least 2 and at most 36. + -- @return A new bint or nil in case the conversion failed. + function bint.frombase(s, base) + if type(s) ~= 'string' then + return + end + base = base or 10 + if not (base >= 2 and base <= 36) then + -- number base is too large + return + end + local step = getbasestep(base) + if #s < step then + -- string is small, use tonumber (faster) + return bint_frominteger(tonumber(s, base)) + end + local sign, int = s:lower():match('^([+-]?)(%w+)$') + if not (sign and int) then + -- invalid integer string representation + return + end + local n = bint_zero() + for i = 1, #int, step do + local part = int:sub(i, i + step - 1) + local d = tonumber(part, base) + if not d then + -- invalid integer string representation + return + end + if i > 1 then + n = n * ipow(1, base, #part) + end + if d ~= 0 then + n:_add(d) + end + end + if sign == '-' then + n:_unm() + end + return n + end + + local bint_frombase = bint.frombase + + --- Create a new bint from a string. + -- The string can by a decimal number, binary number prefixed with '0b' or hexadecimal number prefixed with '0x'. + -- @param s A string convertible to a bint. + -- @return A new bint or nil in case the conversion failed. + -- @see bint.frombase + function bint.fromstring(s) + if type(s) ~= 'string' then + return + end + if s:find('^[+-]?[0-9]+$') then + return bint_frombase(s, 10) + elseif s:find('^[+-]?0[xX][0-9a-fA-F]+$') then + return bint_frombase(s:gsub('0[xX]', '', 1), 16) + elseif s:find('^[+-]?0[bB][01]+$') then + return bint_frombase(s:gsub('0[bB]', '', 1), 2) + end + end + + local bint_fromstring = bint.fromstring + + --- Create a new bint from a buffer of little-endian bytes. + -- @param buffer Buffer of bytes, extra bytes are trimmed from the right, missing bytes are padded to the right. + -- @raise An assert is thrown in case buffer is not an string. + -- @return A bint. + function bint.fromle(buffer) + assert(type(buffer) == 'string', 'buffer is not a string') + if #buffer > BINT_BYTES then -- trim extra bytes from the right + buffer = buffer:sub(1, BINT_BYTES) + elseif #buffer < BINT_BYTES then -- add missing bytes to the right + buffer = buffer .. ('\x00'):rep(BINT_BYTES - #buffer) + end + return setmetatable({ BINT_LEPACKFMT:unpack(buffer) }, bint) + end + + --- Create a new bint from a buffer of big-endian bytes. + -- @param buffer Buffer of bytes, extra bytes are trimmed from the left, missing bytes are padded to the left. + -- @raise An assert is thrown in case buffer is not an string. + -- @return A bint. + function bint.frombe(buffer) + assert(type(buffer) == 'string', 'buffer is not a string') + if #buffer > BINT_BYTES then -- trim extra bytes from the left + buffer = buffer:sub(-BINT_BYTES, #buffer) + elseif #buffer < BINT_BYTES then -- add missing bytes to the left + buffer = ('\x00'):rep(BINT_BYTES - #buffer) .. buffer + end + return setmetatable({ BINT_LEPACKFMT:unpack(buffer:reverse()) }, bint) + end + + --- Create a new bint from a value. + -- @param x A value convertible to a bint (string, number or another bint). + -- @return A new bint, guaranteed to be a new reference in case needed. + -- @raise An assert is thrown in case x is not convertible to a bint. + -- @see bint.tobint + -- @see bint.parse + function bint.new(x) + if getmetatable(x) ~= bint then + local ty = type(x) + if ty == 'number' then + x = bint_frominteger(x) + elseif ty == 'string' then + x = bint_fromstring(x) + end + assert(x, 'value cannot be represented by a bint') + return x + end + -- return a clone + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x[i] + end + return n + end + + local bint_new = bint.new + + --- Convert a value to a bint if possible. + -- @param x A value to be converted (string, number or another bint). + -- @param[opt] clone A boolean that tells if a new bint reference should be returned. + -- Defaults to false. + -- @return A bint or nil in case the conversion failed. + -- @see bint.new + -- @see bint.parse + function bint.tobint(x, clone) + if getmetatable(x) == bint then + if not clone then + return x + end + -- return a clone + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x[i] + end + return n + end + local ty = type(x) + if ty == 'number' then + return bint_frominteger(x) + elseif ty == 'string' then + return bint_fromstring(x) + end + end + + local tobint = bint.tobint + + --- Convert a value to a bint if possible otherwise to a lua number. + -- Useful to prepare values that you are unsure if it's going to be an integer or float. + -- @param x A value to be converted (string, number or another bint). + -- @param[opt] clone A boolean that tells if a new bint reference should be returned. + -- Defaults to false. + -- @return A bint or a lua number or nil in case the conversion failed. + -- @see bint.new + -- @see bint.tobint + function bint.parse(x, clone) + local i = tobint(x, clone) + if i then + return i + end + return tonumber(x) + end + + local bint_parse = bint.parse + + --- Convert a bint to an unsigned integer. + -- Note that large unsigned integers may be represented as negatives in lua integers. + -- Note that lua cannot represent values larger than 64 bits, + -- in that case integer values wrap around. + -- @param x A bint or a number to be converted into an unsigned integer. + -- @return An integer or nil in case the input cannot be represented by an integer. + -- @see bint.tointeger + function bint.touinteger(x) + if getmetatable(x) == bint then + local n = 0 + for i = 1, BINT_SIZE do + n = n | (x[i] << (BINT_WORDBITS * (i - 1))) + end + return n + end + return tointeger(x) + end + + --- Convert a bint to a signed integer. + -- It works by taking absolute values then applying the sign bit in case needed. + -- Note that lua cannot represent values larger than 64 bits, + -- in that case integer values wrap around. + -- @param x A bint or value to be converted into an unsigned integer. + -- @return An integer or nil in case the input cannot be represented by an integer. + -- @see bint.touinteger + function bint.tointeger(x) + if getmetatable(x) == bint then + local n = 0 + local neg = x:isneg() + if neg then + x = -x + end + for i = 1, BINT_SIZE do + n = n | (x[i] << (BINT_WORDBITS * (i - 1))) + end + if neg then + n = -n + end + return n + end + return tointeger(x) + end + + local bint_tointeger = bint.tointeger + + local function bint_assert_tointeger(x) + x = bint_tointeger(x) + if not x then + error('value has no integer representation') + end + return x + end + + --- Convert a bint to a lua float in case integer would wrap around or lua integer otherwise. + -- Different from @{bint.tointeger} the operation does not wrap around integers, + -- but digits precision are lost in the process of converting to a float. + -- @param x A bint or value to be converted into a lua number. + -- @return A lua number or nil in case the input cannot be represented by a number. + -- @see bint.tointeger + function bint.tonumber(x) + if getmetatable(x) == bint then + if x <= BINT_MATHMAXINTEGER and x >= BINT_MATHMININTEGER then + return x:tointeger() + end + return tonumber(tostring(x)) + end + return tonumber(x) + end + + local bint_tonumber = bint.tonumber + + -- Compute base letters to use in bint.tobase + local BASE_LETTERS = {} + do + for i = 1, 36 do + BASE_LETTERS[i - 1] = ('0123456789abcdefghijklmnopqrstuvwxyz'):sub(i, i) + end + end + + --- Convert a bint to a string in the desired base. + -- @param x The bint to be converted from. + -- @param[opt] base Base to be represented, defaults to 10. + -- Must be at least 2 and at most 36. + -- @param[opt] unsigned Whether to output as an unsigned integer. + -- Defaults to false for base 10 and true for others. + -- When unsigned is false the symbol '-' is prepended in negative values. + -- @return A string representing the input. + -- @raise An assert is thrown in case the base is invalid. + function bint.tobase(x, base, unsigned) + x = tobint(x) + if not x then + -- x is a fractional float or something else + return + end + base = base or 10 + if not (base >= 2 and base <= 36) then + -- number base is too large + return + end + if unsigned == nil then + unsigned = base ~= 10 + end + local isxneg = x:isneg() + if (base == 10 and not unsigned) or (base == 16 and unsigned and not isxneg) then + if x <= BINT_MATHMAXINTEGER and x >= BINT_MATHMININTEGER then + -- integer is small, use tostring or string.format (faster) + local n = x:tointeger() + if base == 10 then + return tostring(n) + elseif unsigned then + return string_format('%x', n) + end + end + end + local ss = {} + local neg = not unsigned and isxneg + x = neg and x:abs() or bint_new(x) + local xiszero = x:iszero() + if xiszero then + return '0' + end + -- calculate basepow + local step = 0 + local basepow = 1 + local limit = (BINT_WORDMSB - 1) // base + repeat + step = step + 1 + basepow = basepow * base + until basepow >= limit + -- serialize base digits + local size = BINT_SIZE + local xd, carry, d + repeat + -- single word division + carry = 0 + xiszero = true + for i = size, 1, -1 do + carry = carry | x[i] + d, xd = carry // basepow, carry % basepow + if xiszero and d ~= 0 then + size = i + xiszero = false + end + x[i] = d + carry = xd << BINT_WORDBITS + end + -- digit division + for _ = 1, step do + xd, d = xd // base, xd % base + if xiszero and xd == 0 and d == 0 then + -- stop on leading zeros + break + end + table_insert(ss, 1, BASE_LETTERS[d]) + end + until xiszero + if neg then + table_insert(ss, 1, '-') + end + return table_concat(ss) + end + + local function bint_assert_convert(x) + return assert(tobint(x), 'value has not integer representation') + end + + --- Convert a bint to a buffer of little-endian bytes. + -- @param x A bint or lua integer. + -- @param[opt] trim If true, zero bytes on the right are trimmed. + -- @return A buffer of bytes representing the input. + -- @raise Asserts in case input is not convertible to an integer. + function bint.tole(x, trim) + x = bint_assert_convert(x) + local s = BINT_LEPACKFMT:pack(table_unpack(x)) + if trim then + s = s:gsub('\x00+$', '') + if s == '' then + s = '\x00' + end + end + return s + end + + --- Convert a bint to a buffer of big-endian bytes. + -- @param x A bint or lua integer. + -- @param[opt] trim If true, zero bytes on the left are trimmed. + -- @return A buffer of bytes representing the input. + -- @raise Asserts in case input is not convertible to an integer. + function bint.tobe(x, trim) + x = bint_assert_convert(x) + local s = BINT_LEPACKFMT:pack(table_unpack(x)):reverse() + if trim then + s = s:gsub('^\x00+', '') + if s == '' then + s = '\x00' + end + end + return s + end + + --- Check if a number is 0 considering bints. + -- @param x A bint or a lua number. + function bint.iszero(x) + if getmetatable(x) == bint then + for i = 1, BINT_SIZE do + if x[i] ~= 0 then + return false + end + end + return true + end + return x == 0 + end + + --- Check if a number is 1 considering bints. + -- @param x A bint or a lua number. + function bint.isone(x) + if getmetatable(x) == bint then + if x[1] ~= 1 then + return false + end + for i = 2, BINT_SIZE do + if x[i] ~= 0 then + return false + end + end + return true + end + return x == 1 + end + + --- Check if a number is -1 considering bints. + -- @param x A bint or a lua number. + function bint.isminusone(x) + if getmetatable(x) == bint then + for i = 1, BINT_SIZE do + if x[i] ~= BINT_WORDMAX then + return false + end + end + return true + end + return x == -1 + end + + local bint_isminusone = bint.isminusone + + --- Check if the input is a bint. + -- @param x Any lua value. + function bint.isbint(x) + return getmetatable(x) == bint + end + + --- Check if the input is a lua integer or a bint. + -- @param x Any lua value. + function bint.isintegral(x) + return getmetatable(x) == bint or math_type(x) == 'integer' + end + + --- Check if the input is a bint or a lua number. + -- @param x Any lua value. + function bint.isnumeric(x) + return getmetatable(x) == bint or type(x) == 'number' + end + + --- Get the number type of the input (bint, integer or float). + -- @param x Any lua value. + -- @return Returns "bint" for bints, "integer" for lua integers, + -- "float" from lua floats or nil otherwise. + function bint.type(x) + if getmetatable(x) == bint then + return 'bint' + end + return math_type(x) + end + + --- Check if a number is negative considering bints. + -- Zero is guaranteed to never be negative for bints. + -- @param x A bint or a lua number. + function bint.isneg(x) + if getmetatable(x) == bint then + return x[BINT_SIZE] & BINT_WORDMSB ~= 0 + end + return x < 0 + end + + local bint_isneg = bint.isneg + + --- Check if a number is positive considering bints. + -- @param x A bint or a lua number. + function bint.ispos(x) + if getmetatable(x) == bint then + return not x:isneg() and not x:iszero() + end + return x > 0 + end + + --- Check if a number is even considering bints. + -- @param x A bint or a lua number. + function bint.iseven(x) + if getmetatable(x) == bint then + return x[1] & 1 == 0 + end + return math_abs(x) % 2 == 0 + end + + --- Check if a number is odd considering bints. + -- @param x A bint or a lua number. + function bint.isodd(x) + if getmetatable(x) == bint then + return x[1] & 1 == 1 + end + return math_abs(x) % 2 == 1 + end + + --- Create a new bint with the maximum possible integer value. + function bint.maxinteger() + local x = setmetatable({}, bint) + for i = 1, BINT_SIZE - 1 do + x[i] = BINT_WORDMAX + end + x[BINT_SIZE] = BINT_WORDMAX ~ BINT_WORDMSB + return x + end + + --- Create a new bint with the minimum possible integer value. + function bint.mininteger() + local x = setmetatable({}, bint) + for i = 1, BINT_SIZE - 1 do + x[i] = 0 + end + x[BINT_SIZE] = BINT_WORDMSB + return x + end + + --- Bitwise left shift a bint in one bit (in-place). + function bint:_shlone() + local wordbitsm1 = BINT_WORDBITS - 1 + for i = BINT_SIZE, 2, -1 do + self[i] = ((self[i] << 1) | (self[i - 1] >> wordbitsm1)) & BINT_WORDMAX + end + self[1] = (self[1] << 1) & BINT_WORDMAX + return self + end + + --- Bitwise right shift a bint in one bit (in-place). + function bint:_shrone() + local wordbitsm1 = BINT_WORDBITS - 1 + for i = 1, BINT_SIZE - 1 do + self[i] = ((self[i] >> 1) | (self[i + 1] << wordbitsm1)) & BINT_WORDMAX + end + self[BINT_SIZE] = self[BINT_SIZE] >> 1 + return self + end + + -- Bitwise left shift words of a bint (in-place). Used only internally. + function bint:_shlwords(n) + for i = BINT_SIZE, n + 1, -1 do + self[i] = self[i - n] + end + for i = 1, n do + self[i] = 0 + end + return self + end + + -- Bitwise right shift words of a bint (in-place). Used only internally. + function bint:_shrwords(n) + if n < BINT_SIZE then + for i = 1, BINT_SIZE - n do + self[i] = self[i + n] + end + for i = BINT_SIZE - n + 1, BINT_SIZE do + self[i] = 0 + end + else + for i = 1, BINT_SIZE do + self[i] = 0 + end + end + return self + end + + --- Increment a bint by one (in-place). + function bint:_inc() + for i = 1, BINT_SIZE do + local tmp = self[i] + local v = (tmp + 1) & BINT_WORDMAX + self[i] = v + if v > tmp then + break + end + end + return self + end + + --- Increment a number by one considering bints. + -- @param x A bint or a lua number to increment. + function bint.inc(x) + local ix = tobint(x, true) + if ix then + return ix:_inc() + end + return x + 1 + end + + --- Decrement a bint by one (in-place). + function bint:_dec() + for i = 1, BINT_SIZE do + local tmp = self[i] + local v = (tmp - 1) & BINT_WORDMAX + self[i] = v + if v <= tmp then + break + end + end + return self + end + + --- Decrement a number by one considering bints. + -- @param x A bint or a lua number to decrement. + function bint.dec(x) + local ix = tobint(x, true) + if ix then + return ix:_dec() + end + return x - 1 + end + + --- Assign a bint to a new value (in-place). + -- @param y A value to be copied from. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_assign(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = y[i] + end + return self + end + + --- Take absolute of a bint (in-place). + function bint:_abs() + if self:isneg() then + self:_unm() + end + return self + end + + --- Take absolute of a number considering bints. + -- @param x A bint or a lua number to take the absolute. + function bint.abs(x) + local ix = tobint(x, true) + if ix then + return ix:_abs() + end + return math_abs(x) + end + + local bint_abs = bint.abs + + --- Take the floor of a number considering bints. + -- @param x A bint or a lua number to perform the floor operation. + function bint.floor(x) + if getmetatable(x) == bint then + return bint_new(x) + end + return bint_new(math_floor(tonumber(x))) + end + + --- Take ceil of a number considering bints. + -- @param x A bint or a lua number to perform the ceil operation. + function bint.ceil(x) + if getmetatable(x) == bint then + return bint_new(x) + end + return bint_new(math_ceil(tonumber(x))) + end + + --- Wrap around bits of an integer (discarding left bits) considering bints. + -- @param x A bint or a lua integer. + -- @param y Number of right bits to preserve. + function bint.bwrap(x, y) + x = bint_assert_convert(x) + if y <= 0 then + return bint_zero() + elseif y < BINT_BITS then + return x & (bint_one() << y):_dec() + end + return bint_new(x) + end + + --- Rotate left integer x by y bits considering bints. + -- @param x A bint or a lua integer. + -- @param y Number of bits to rotate. + function bint.brol(x, y) + x, y = bint_assert_convert(x), bint_assert_tointeger(y) + if y > 0 then + return (x << y) | (x >> (BINT_BITS - y)) + elseif y < 0 then + if y ~= math_mininteger then + return x:bror(-y) + else + x:bror(-(y + 1)) + x:bror(1) + end + end + return x + end + + --- Rotate right integer x by y bits considering bints. + -- @param x A bint or a lua integer. + -- @param y Number of bits to rotate. + function bint.bror(x, y) + x, y = bint_assert_convert(x), bint_assert_tointeger(y) + if y > 0 then + return (x >> y) | (x << (BINT_BITS - y)) + elseif y < 0 then + if y ~= math_mininteger then + return x:brol(-y) + else + x:brol(-(y + 1)) + x:brol(1) + end + end + return x + end + + --- Truncate a number to a bint. + -- Floats numbers are truncated, that is, the fractional port is discarded. + -- @param x A number to truncate. + -- @return A new bint or nil in case the input does not fit in a bint or is not a number. + function bint.trunc(x) + if getmetatable(x) ~= bint then + x = tonumber(x) + if x then + local ty = math_type(x) + if ty == 'float' then + -- truncate to integer + x = math_modf(x) + end + return bint_frominteger(x) + end + return + end + return bint_new(x) + end + + --- Take maximum between two numbers considering bints. + -- @param x A bint or lua number to compare. + -- @param y A bint or lua number to compare. + -- @return A bint or a lua number. Guarantees to return a new bint for integer values. + function bint.max(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + return bint_new(ix > iy and ix or iy) + end + return bint_parse(math_max(x, y)) + end + + --- Take minimum between two numbers considering bints. + -- @param x A bint or lua number to compare. + -- @param y A bint or lua number to compare. + -- @return A bint or a lua number. Guarantees to return a new bint for integer values. + function bint.min(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + return bint_new(ix < iy and ix or iy) + end + return bint_parse(math_min(x, y)) + end + + --- Add an integer to a bint (in-place). + -- @param y An integer to be added. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_add(y) + y = bint_assert_convert(y) + local carry = 0 + for i = 1, BINT_SIZE do + local tmp = self[i] + y[i] + carry + carry = tmp >> BINT_WORDBITS + self[i] = tmp & BINT_WORDMAX + end + return self + end + + --- Add two numbers considering bints. + -- @param x A bint or a lua number to be added. + -- @param y A bint or a lua number to be added. + function bint.__add(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local z = setmetatable({}, bint) + local carry = 0 + for i = 1, BINT_SIZE do + local tmp = ix[i] + iy[i] + carry + carry = tmp >> BINT_WORDBITS + z[i] = tmp & BINT_WORDMAX + end + return z + end + return bint_tonumber(x) + bint_tonumber(y) + end + + --- Subtract an integer from a bint (in-place). + -- @param y An integer to subtract. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_sub(y) + y = bint_assert_convert(y) + local borrow = 0 + local wordmaxp1 = BINT_WORDMAX + 1 + for i = 1, BINT_SIZE do + local res = self[i] + wordmaxp1 - y[i] - borrow + self[i] = res & BINT_WORDMAX + borrow = (res >> BINT_WORDBITS) ~ 1 + end + return self + end + + --- Subtract two numbers considering bints. + -- @param x A bint or a lua number to be subtracted from. + -- @param y A bint or a lua number to subtract. + function bint.__sub(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local z = setmetatable({}, bint) + local borrow = 0 + local wordmaxp1 = BINT_WORDMAX + 1 + for i = 1, BINT_SIZE do + local res = ix[i] + wordmaxp1 - iy[i] - borrow + z[i] = res & BINT_WORDMAX + borrow = (res >> BINT_WORDBITS) ~ 1 + end + return z + end + return bint_tonumber(x) - bint_tonumber(y) + end + + --- Multiply two numbers considering bints. + -- @param x A bint or a lua number to multiply. + -- @param y A bint or a lua number to multiply. + function bint.__mul(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local z = bint_zero() + local sizep1 = BINT_SIZE + 1 + local s = sizep1 + local e = 0 + for i = 1, BINT_SIZE do + if ix[i] ~= 0 or iy[i] ~= 0 then + e = math_max(e, i) + s = math_min(s, i) + end + end + for i = s, e do + for j = s, math_min(sizep1 - i, e) do + local a = ix[i] * iy[j] + if a ~= 0 then + local carry = 0 + for k = i + j - 1, BINT_SIZE do + local tmp = z[k] + (a & BINT_WORDMAX) + carry + carry = tmp >> BINT_WORDBITS + z[k] = tmp & BINT_WORDMAX + a = a >> BINT_WORDBITS + end + end + end + end + return z + end + return bint_tonumber(x) * bint_tonumber(y) + end + + --- Check if bints are equal. + -- @param x A bint to compare. + -- @param y A bint to compare. + function bint.__eq(x, y) + for i = 1, BINT_SIZE do + if x[i] ~= y[i] then + return false + end + end + return true + end + + --- Check if numbers are equal considering bints. + -- @param x A bint or lua number to compare. + -- @param y A bint or lua number to compare. + function bint.eq(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + return ix == iy + end + return x == y + end + + local bint_eq = bint.eq + + local function findleftbit(x) + for i = BINT_SIZE, 1, -1 do + local v = x[i] + if v ~= 0 then + local j = 0 + repeat + v = v >> 1 + j = j + 1 + until v == 0 + return (i - 1) * BINT_WORDBITS + j - 1, i + end + end + end + + -- Single word division modulus + local function sudivmod(nume, deno) + local rema + local carry = 0 + for i = BINT_SIZE, 1, -1 do + carry = carry | nume[i] + nume[i] = carry // deno + rema = carry % deno + carry = rema << BINT_WORDBITS + end + return rema + end + + --- Perform unsigned division and modulo operation between two integers considering bints. + -- This is effectively the same of @{bint.udiv} and @{bint.umod}. + -- @param x The numerator, must be a bint or a lua integer. + -- @param y The denominator, must be a bint or a lua integer. + -- @return The quotient following the remainder, both bints. + -- @raise Asserts on attempt to divide by zero + -- or if inputs are not convertible to integers. + -- @see bint.udiv + -- @see bint.umod + function bint.udivmod(x, y) + local nume = bint_new(x) + local deno = bint_assert_convert(y) + -- compute if high bits of denominator are all zeros + local ishighzero = true + for i = 2, BINT_SIZE do + if deno[i] ~= 0 then + ishighzero = false + break + end + end + if ishighzero then + -- try to divide by a single word (optimization) + local low = deno[1] + assert(low ~= 0, 'attempt to divide by zero') + if low == 1 then + -- denominator is one + return nume, bint_zero() + elseif low <= (BINT_WORDMSB - 1) then + -- can do single word division + local rema = sudivmod(nume, low) + return nume, bint_fromuinteger(rema) + end + end + if nume:ult(deno) then + -- denominator is greater than numerator + return bint_zero(), nume + end + -- align leftmost digits in numerator and denominator + local denolbit = findleftbit(deno) + local numelbit, numesize = findleftbit(nume) + local bit = numelbit - denolbit + deno = deno << bit + local wordmaxp1 = BINT_WORDMAX + 1 + local wordbitsm1 = BINT_WORDBITS - 1 + local denosize = numesize + local quot = bint_zero() + while bit >= 0 do + -- compute denominator <= numerator + local le = true + local size = math_max(numesize, denosize) + for i = size, 1, -1 do + local a, b = deno[i], nume[i] + if a ~= b then + le = a < b + break + end + end + -- if the portion of the numerator above the denominator is greater or equal than to the denominator + if le then + -- subtract denominator from the portion of the numerator + local borrow = 0 + for i = 1, size do + local res = nume[i] + wordmaxp1 - deno[i] - borrow + nume[i] = res & BINT_WORDMAX + borrow = (res >> BINT_WORDBITS) ~ 1 + end + -- concatenate 1 to the right bit of the quotient + local i = (bit // BINT_WORDBITS) + 1 + quot[i] = quot[i]| (1 << (bit % BINT_WORDBITS)) + end + -- shift right the denominator in one bit + for i = 1, denosize - 1 do + deno[i] = ((deno[i] >> 1) | (deno[i + 1] << wordbitsm1)) & BINT_WORDMAX + end + local lastdenoword = deno[denosize] >> 1 + deno[denosize] = lastdenoword + -- recalculate denominator size (optimization) + if lastdenoword == 0 then + while deno[denosize] == 0 do + denosize = denosize - 1 + end + if denosize == 0 then + break + end + end + -- decrement current set bit for the quotient + bit = bit - 1 + end + -- the remaining numerator is the remainder + return quot, nume + end + + local bint_udivmod = bint.udivmod + + --- Perform unsigned division between two integers considering bints. + -- @param x The numerator, must be a bint or a lua integer. + -- @param y The denominator, must be a bint or a lua integer. + -- @return The quotient, a bint. + -- @raise Asserts on attempt to divide by zero + -- or if inputs are not convertible to integers. + function bint.udiv(x, y) + return (bint_udivmod(x, y)) + end + + --- Perform unsigned integer modulo operation between two integers considering bints. + -- @param x The numerator, must be a bint or a lua integer. + -- @param y The denominator, must be a bint or a lua integer. + -- @return The remainder, a bint. + -- @raise Asserts on attempt to divide by zero + -- or if the inputs are not convertible to integers. + function bint.umod(x, y) + local _, rema = bint_udivmod(x, y) + return rema + end + + local bint_umod = bint.umod + + --- Perform integer truncate division and modulo operation between two numbers considering bints. + -- This is effectively the same of @{bint.tdiv} and @{bint.tmod}. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient following the remainder, both bint or lua number. + -- @raise Asserts on attempt to divide by zero or on division overflow. + -- @see bint.tdiv + -- @see bint.tmod + function bint.tdivmod(x, y) + local ax, ay = bint_abs(x), bint_abs(y) + local ix, iy = tobint(ax), tobint(ay) + local quot, rema + if ix and iy then + assert(not (bint_eq(x, BINT_MININTEGER) and bint_isminusone(y)), 'division overflow') + quot, rema = bint_udivmod(ix, iy) + else + quot, rema = ax // ay, ax % ay + end + local isxneg, isyneg = bint_isneg(x), bint_isneg(y) + if isxneg ~= isyneg then + quot = -quot + end + if isxneg then + rema = -rema + end + return quot, rema + end + + local bint_tdivmod = bint.tdivmod + + --- Perform truncate division between two numbers considering bints. + -- Truncate division is a division that rounds the quotient towards zero. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient, a bint or lua number. + -- @raise Asserts on attempt to divide by zero or on division overflow. + function bint.tdiv(x, y) + return (bint_tdivmod(x, y)) + end + + --- Perform integer truncate modulo operation between two numbers considering bints. + -- The operation is defined as the remainder of the truncate division + -- (division that rounds the quotient towards zero). + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The remainder, a bint or lua number. + -- @raise Asserts on attempt to divide by zero or on division overflow. + function bint.tmod(x, y) + local _, rema = bint_tdivmod(x, y) + return rema + end + + --- Perform integer floor division and modulo operation between two numbers considering bints. + -- This is effectively the same of @{bint.__idiv} and @{bint.__mod}. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient following the remainder, both bint or lua number. + -- @raise Asserts on attempt to divide by zero. + -- @see bint.__idiv + -- @see bint.__mod + function bint.idivmod(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local isnumeneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local isdenoneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if isnumeneg then + ix = -ix + end + if isdenoneg then + iy = -iy + end + local quot, rema = bint_udivmod(ix, iy) + if isnumeneg ~= isdenoneg then + quot:_unm() + -- round quotient towards minus infinity + if not rema:iszero() then + quot:_dec() + -- adjust the remainder + if isnumeneg and not isdenoneg then + rema:_unm():_add(y) + elseif isdenoneg and not isnumeneg then + rema:_add(y) + end + end + elseif isnumeneg then + -- adjust the remainder + rema:_unm() + end + return quot, rema + end + local nx, ny = bint_tonumber(x), bint_tonumber(y) + return nx // ny, nx % ny + end + + local bint_idivmod = bint.idivmod + + --- Perform floor division between two numbers considering bints. + -- Floor division is a division that rounds the quotient towards minus infinity, + -- resulting in the floor of the division of its operands. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient, a bint or lua number. + -- @raise Asserts on attempt to divide by zero. + function bint.__idiv(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local isnumeneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local isdenoneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if isnumeneg then + ix = -ix + end + if isdenoneg then + iy = -iy + end + local quot, rema = bint_udivmod(ix, iy) + if isnumeneg ~= isdenoneg then + quot:_unm() + -- round quotient towards minus infinity + if not rema:iszero() then + quot:_dec() + end + end + return quot, rema + end + return bint_tonumber(x) // bint_tonumber(y) + end + + --- Perform division between two numbers considering bints. + -- This always casts inputs to floats, for integer division only use @{bint.__idiv}. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient, a lua number. + function bint.__div(x, y) + return bint_tonumber(x) / bint_tonumber(y) + end + + --- Perform integer floor modulo operation between two numbers considering bints. + -- The operation is defined as the remainder of the floor division + -- (division that rounds the quotient towards minus infinity). + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The remainder, a bint or lua number. + -- @raise Asserts on attempt to divide by zero. + function bint.__mod(x, y) + local _, rema = bint_idivmod(x, y) + return rema + end + + --- Perform integer power between two integers considering bints. + -- If y is negative then pow is performed as an unsigned integer. + -- @param x The base, an integer. + -- @param y The exponent, an integer. + -- @return The result of the pow operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__pow + -- @see bint.upowmod + function bint.ipow(x, y) + y = bint_assert_convert(y) + if y:iszero() then + return bint_one() + elseif y:isone() then + return bint_new(x) + end + -- compute exponentiation by squaring + x, y = bint_new(x), bint_new(y) + local z = bint_one() + repeat + if y:iseven() then + x = x * x + y:_shrone() + else + z = x * z + x = x * x + y:_dec():_shrone() + end + until y:isone() + return x * z + end + + --- Perform integer power between two unsigned integers over a modulus considering bints. + -- @param x The base, an integer. + -- @param y The exponent, an integer. + -- @param m The modulus, an integer. + -- @return The result of the pow operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__pow + -- @see bint.ipow + function bint.upowmod(x, y, m) + m = bint_assert_convert(m) + if m:isone() then + return bint_zero() + end + x, y = bint_new(x), bint_new(y) + local z = bint_one() + x = bint_umod(x, m) + while not y:iszero() do + if y:isodd() then + z = bint_umod(z * x, m) + end + y:_shrone() + x = bint_umod(x * x, m) + end + return z + end + + --- Perform numeric power between two numbers considering bints. + -- This always casts inputs to floats, for integer power only use @{bint.ipow}. + -- @param x The base, a bint or lua number. + -- @param y The exponent, a bint or lua number. + -- @return The result of the pow operation, a lua number. + -- @see bint.ipow + function bint.__pow(x, y) + return bint_tonumber(x) ^ bint_tonumber(y) + end + + --- Bitwise left shift integers considering bints. + -- @param x An integer to perform the bitwise shift. + -- @param y An integer with the number of bits to shift. + -- @return The result of shift operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__shl(x, y) + x, y = bint_new(x), bint_assert_tointeger(y) + if y == math_mininteger or math_abs(y) >= BINT_BITS then + return bint_zero() + end + if y < 0 then + return x >> -y + end + local nvals = y // BINT_WORDBITS + if nvals ~= 0 then + x:_shlwords(nvals) + y = y - nvals * BINT_WORDBITS + end + if y ~= 0 then + local wordbitsmy = BINT_WORDBITS - y + for i = BINT_SIZE, 2, -1 do + x[i] = ((x[i] << y) | (x[i - 1] >> wordbitsmy)) & BINT_WORDMAX + end + x[1] = (x[1] << y) & BINT_WORDMAX + end + return x + end + + --- Bitwise right shift integers considering bints. + -- @param x An integer to perform the bitwise shift. + -- @param y An integer with the number of bits to shift. + -- @return The result of shift operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__shr(x, y) + x, y = bint_new(x), bint_assert_tointeger(y) + if y == math_mininteger or math_abs(y) >= BINT_BITS then + return bint_zero() + end + if y < 0 then + return x << -y + end + local nvals = y // BINT_WORDBITS + if nvals ~= 0 then + x:_shrwords(nvals) + y = y - nvals * BINT_WORDBITS + end + if y ~= 0 then + local wordbitsmy = BINT_WORDBITS - y + for i = 1, BINT_SIZE - 1 do + x[i] = ((x[i] >> y) | (x[i + 1] << wordbitsmy)) & BINT_WORDMAX + end + x[BINT_SIZE] = x[BINT_SIZE] >> y + end + return x + end + + --- Bitwise AND bints (in-place). + -- @param y An integer to perform bitwise AND. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_band(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = self[i] & y[i] + end + return self + end + + --- Bitwise AND two integers considering bints. + -- @param x An integer to perform bitwise AND. + -- @param y An integer to perform bitwise AND. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__band(x, y) + return bint_new(x):_band(y) + end + + --- Bitwise OR bints (in-place). + -- @param y An integer to perform bitwise OR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_bor(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = self[i]| y[i] + end + return self + end + + --- Bitwise OR two integers considering bints. + -- @param x An integer to perform bitwise OR. + -- @param y An integer to perform bitwise OR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__bor(x, y) + return bint_new(x):_bor(y) + end + + --- Bitwise XOR bints (in-place). + -- @param y An integer to perform bitwise XOR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_bxor(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = self[i] ~ y[i] + end + return self + end + + --- Bitwise XOR two integers considering bints. + -- @param x An integer to perform bitwise XOR. + -- @param y An integer to perform bitwise XOR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__bxor(x, y) + return bint_new(x):_bxor(y) + end + + --- Bitwise NOT a bint (in-place). + function bint:_bnot() + for i = 1, BINT_SIZE do + self[i] = (~self[i]) & BINT_WORDMAX + end + return self + end + + --- Bitwise NOT a bint. + -- @param x An integer to perform bitwise NOT. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__bnot(x) + local y = setmetatable({}, bint) + for i = 1, BINT_SIZE do + y[i] = (~x[i]) & BINT_WORDMAX + end + return y + end + + --- Negate a bint (in-place). This effectively applies two's complements. + function bint:_unm() + return self:_bnot():_inc() + end + + --- Negate a bint. This effectively applies two's complements. + -- @param x A bint to perform negation. + function bint.__unm(x) + return (~x):_inc() + end + + --- Compare if integer x is less than y considering bints (unsigned version). + -- @param x Left integer to compare. + -- @param y Right integer to compare. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__lt + function bint.ult(x, y) + x, y = bint_assert_convert(x), bint_assert_convert(y) + for i = BINT_SIZE, 1, -1 do + local a, b = x[i], y[i] + if a ~= b then + return a < b + end + end + return false + end + + --- Compare if bint x is less or equal than y considering bints (unsigned version). + -- @param x Left integer to compare. + -- @param y Right integer to compare. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__le + function bint.ule(x, y) + x, y = bint_assert_convert(x), bint_assert_convert(y) + for i = BINT_SIZE, 1, -1 do + local a, b = x[i], y[i] + if a ~= b then + return a < b + end + end + return true + end + + --- Compare if number x is less than y considering bints and signs. + -- @param x Left value to compare, a bint or lua number. + -- @param y Right value to compare, a bint or lua number. + -- @see bint.ult + function bint.__lt(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local xneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local yneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if xneg == yneg then + for i = BINT_SIZE, 1, -1 do + local a, b = ix[i], iy[i] + if a ~= b then + return a < b + end + end + return false + end + return xneg and not yneg + end + return bint_tonumber(x) < bint_tonumber(y) + end + + --- Compare if number x is less or equal than y considering bints and signs. + -- @param x Left value to compare, a bint or lua number. + -- @param y Right value to compare, a bint or lua number. + -- @see bint.ule + function bint.__le(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local xneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local yneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if xneg == yneg then + for i = BINT_SIZE, 1, -1 do + local a, b = ix[i], iy[i] + if a ~= b then + return a < b + end + end + return true + end + return xneg and not yneg + end + return bint_tonumber(x) <= bint_tonumber(y) + end + + --- Convert a bint to a string on base 10. + -- @see bint.tobase + function bint:__tostring() + return self:tobase(10) + end + + -- Allow creating bints by calling bint itself + setmetatable(bint, { + __call = function(_, x) + return bint_new(x) + end + }) + + BINT_MATHMININTEGER, BINT_MATHMAXINTEGER = bint_new(math.mininteger), bint_new(math.maxinteger) + BINT_MININTEGER = bint.mininteger() + memo[memoindex] = bint + + return bint +end + +return newmodule diff --git a/onchain/permissionless-arbitration/offchain/utils/color.lua b/onchain/permissionless-arbitration/offchain/utils/color.lua new file mode 100644 index 000000000..c188228c0 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/utils/color.lua @@ -0,0 +1,133 @@ +-- color.lua + +-------------------------------------------------------------------------------- + +-- A super-simple way to make colored text output in Lua. +-- To use, simply print out things from this module, then print out some text. +-- +-- Example: +-- print(color.bg.green .. color.fg.RED .. "This is bright red on green") +-- print(color.invert .. "This is inverted..." .. color.reset .. " And this isn't.") +-- print(color.fg(0xDE) .. color.bg(0xEE) .. "You can use xterm-256 colors too!" .. color.reset) +-- print("And also " .. color.bold .. "BOLD" .. color.normal .. " if you want.") +-- print(color.bold .. color.fg.BLUE .. color.bg.blue .. "Miss your " .. color.fg.RED .. "C-64" .. color.fg.BLUE .. "?" .. color.reset) +-- +-- You can see all these examples in action by calling color.test() +-- +-- Can't pick a good color scheme? Look at a handy chart: +-- print(color.chart()) +-- +-- If you want to add anything to this, check out the Wikipedia page on ANSI control codes: +-- http://en.wikipedia.org/wiki/ANSI_escape_code + +-------------------------------------------------------------------------------- + +-- Copyright (C) 2012 Ross Andrews +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Lesser General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with this program. If not, see . + +-------------------------------------------------------------------------------- + +-- A note about licensing: +-- +-- The LGPL isn't really intended to be used with non-compiled libraries. The way +-- I interpret derivative works of this library is this: if you don't modify this +-- file, and the program it's embedded in doesn't modify the Lua table it defines, +-- then you can distribute it with a program under any license. If you do either +-- of those things, then you've created a derivative work of this library and you +-- have to release the modifications you made under this same license. + +local color = { _NAME = "color" } +local _M = color + +local esc = string.char(27, 91) + +local names = {'black', 'red', 'green', 'yellow', 'blue', 'pink', 'cyan', 'white'} +local hi_names = {'BLACK', 'RED', 'GREEN', 'YELLOW', 'BLUE', 'PINK', 'CYAN', 'WHITE'} + +color.fg, color.bg = {}, {} + +for i, name in ipairs(names) do + color.fg[name] = esc .. tostring(30+i-1) .. 'm' + _M[name] = color.fg[name] + color.bg[name] = esc .. tostring(40+i-1) .. 'm' +end + +for i, name in ipairs(hi_names) do + color.fg[name] = esc .. tostring(90+i-1) .. 'm' + _M[name] = color.fg[name] + color.bg[name] = esc .. tostring(100+i-1) .. 'm' +end + +local function fg256(_,n) + return esc .. "38;5;" .. n .. 'm' +end + +local function bg256(_,n) + return esc .. "48;5;" .. n .. 'm' +end + +setmetatable(color.fg, {__call = fg256}) +setmetatable(color.bg, {__call = bg256}) + +color.reset = esc .. '0m' +color.clear = esc .. '2J' + +color.bold = esc .. '1m' +color.faint = esc .. '2m' +color.normal = esc .. '22m' +color.invert = esc .. '7m' +color.underline = esc .. '4m' + +color.hide = esc .. '?25l' +color.show = esc .. '?25h' + +function color.move(x, y) + return esc .. y .. ';' .. x .. 'H' +end + +color.home = color.move(1, 1) + +-------------------------------------------------- + +function color.chart(ch,col) + local cols = '0123456789abcdef' + + ch = ch or ' ' + col = col or color.fg.black + local str = color.reset .. color.bg.WHITE .. col + + for y = 0, 15 do + for x = 0, 15 do + local lbl = cols:sub(x+1, x+1) + if x == 0 then lbl = cols:sub(y+1, y+1) end + + str = str .. color.bg.black .. color.fg.WHITE .. lbl + str = str .. color.bg(x+y*16) .. col .. ch + end + str = str .. color.reset .. "\n" + end + return str .. color.reset +end + +function color.test() + print(color.reset .. color.bg.green .. color.fg.RED .. "This is bright red on green" .. color.reset) + print(color.invert .. "This is inverted..." .. color.reset .. " And this isn't.") + print(color.fg(0xDE) .. color.bg(0xEE) .. "You can use xterm-256 colors too!" .. color.reset) + print("And also " .. color.bold .. "BOLD" .. color.normal .. " if you want.") + print(color.bold .. color.fg.BLUE .. color.bg.blue .. "Miss your " .. color.fg.RED .. "C-64" .. color.fg.BLUE .. "?" .. color.reset) + print("Try printing " .. color.underline .. _M._NAME .. ".chart()" .. color.reset) +end + +return color diff --git a/onchain/permissionless-arbitration/offchain/utils/eth_ebi.lua b/onchain/permissionless-arbitration/offchain/utils/eth_ebi.lua new file mode 100644 index 000000000..467bf8041 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/utils/eth_ebi.lua @@ -0,0 +1,31 @@ +local function encode_sig(sig) + local cmd = string.format([[cast sig-event "%s"]], sig) + + local handle = io.popen(cmd) + assert(handle) + + local encoded_sig = handle:read() + handle:close() + return encoded_sig +end + +local function decode_event_data(sig, data) + local cmd = string.format([[cast --abi-decode "bananas()%s" %s]], sig, data) + + local handle = io.popen(cmd) + assert(handle) + + local decoded_data + local ret = {} + repeat + decoded_data = handle:read() + table.insert(ret, decoded_data) + until not decoded_data + handle:close() + return ret +end + +return { + encode_sig = encode_sig, + decode_event_data = decode_event_data, +} diff --git a/onchain/permissionless-arbitration/offchain/utils/helper.lua b/onchain/permissionless-arbitration/offchain/utils/helper.lua new file mode 100644 index 000000000..8410296b8 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/utils/helper.lua @@ -0,0 +1,94 @@ +local color = require "utils.color" + +local names = {'green', 'yellow', 'blue', 'pink', 'cyan', 'white'} +local idle_template = [[ls player%d_idle 2>/dev/null | grep player%d_idle | wc -l]] +local ps_template = [[ps %s | grep defunct | wc -l]] + +local function log(player_index, msg) + local color_index = (player_index - 1) % #names + 1 + local timestamp = os.date("%m/%d/%Y %X") + print(color.reset .. color.fg[names[color_index]] .. string.format("[#%d][%s] %s", player_index, timestamp, msg) .. color.reset) +end + +local function log_to_ts(reader, last_ts) + -- print everything hold in the buffer which has smaller timestamp + -- this is to synchronise when there're gaps in between the logs + local msg_output = 0 + while true do + local msg = reader:read() + if msg then + msg_output = msg_output + 1 + print(msg) + + local i, j = msg:find("%d%d/%d%d/%d%d%d%d %d%d:%d%d:%d%d") + if i and j then + local timestamp = msg:sub(i, j) + if timestamp > last_ts then + last_ts = timestamp + break + end + end + else + break + end + end + return last_ts, msg_output +end + +local function is_zombie(pid) + local reader = io.popen(string.format(ps_template, pid)) + ret = reader:read() + reader:close() + return tonumber(ret) == 1 +end + +local function stop_players(pid_reader) + for pid, reader in pairs(pid_reader) do + print(string.format("Stopping player with pid %s...", pid)) + os.execute(string.format("kill -15 %s", pid)) + reader:close() + print "Player stopped" + end +end + +local function touch_player_idle(player_index) + os.execute(string.format("touch player%d_idle", player_index)) +end + +local function is_player_idle(player_index) + local reader = io.popen(string.format(idle_template, player_index, player_index)) + ret = reader:read() + reader:close() + return tonumber(ret) == 1 +end + +local function rm_player_idle(player_index) + os.execute(string.format("rm player%d_idle 2>/dev/null", player_index)) +end + +local function all_players_idle(pid_player) + for pid, player in pairs(pid_player) do + if not is_player_idle(player) then + return false + end + end + return true +end + +local function rm_all_players_idle(pid_player) + for pid, player in pairs(pid_player) do + rm_player_idle(player) + end + return true +end + +return { + log = log, + log_to_ts = log_to_ts, + is_zombie = is_zombie, + stop_players = stop_players, + touch_player_idle = touch_player_idle, + all_players_idle = all_players_idle, + rm_all_players_idle = rm_all_players_idle, + rm_player_idle = rm_player_idle +} diff --git a/onchain/permissionless-arbitration/offchain/utils/time.lua b/onchain/permissionless-arbitration/offchain/utils/time.lua new file mode 100644 index 000000000..85e8b2931 --- /dev/null +++ b/onchain/permissionless-arbitration/offchain/utils/time.lua @@ -0,0 +1,8 @@ +local clock = os.clock + +function sleep(number_of_seconds) + local t0 = clock() + while clock() - t0 <= number_of_seconds do end +end + +return {sleep = sleep} diff --git a/onchain/permissionless-arbitration/src/CanonicalConstants.sol b/onchain/permissionless-arbitration/src/CanonicalConstants.sol new file mode 100644 index 000000000..c06a26f3a --- /dev/null +++ b/onchain/permissionless-arbitration/src/CanonicalConstants.sol @@ -0,0 +1,50 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./Time.sol"; + +library ArbitrationConstants { + // maximum tolerance time for participant being censored + // Time.Duration constant CENSORSHIP_TOLERANCE = + // Time.Duration.wrap(60 * 60 * 24 * 7); + + // maximum time for replaying the computation offchain + // Time.Duration constant VALIDATOR_EFFORT = + // Time.Duration.wrap(60 * 60 * 24 * 7); // TODO + + // Dummy + Time.Duration constant VALIDATOR_EFFORT = Time.Duration.wrap(0); + Time.Duration constant CENSORSHIP_TOLERANCE = Time.Duration.wrap(210); + + Time.Duration constant DISPUTE_TIMEOUT = + Time.Duration.wrap( + Time.Duration.unwrap(CENSORSHIP_TOLERANCE) + + Time.Duration.unwrap(VALIDATOR_EFFORT) + ); + + // 4-level tournament + uint64 constant LEVELS = 3; + // uint64 constant LOG2_MAX_MCYCLE = 63; + + /// @return log2step gap of each leaf in the tournament[level] + function log2step(uint64 level) internal pure returns (uint64) { + uint64[LEVELS] memory arr = [ + uint64(31), + uint64(16), + uint64(0) + ]; + return arr[level]; + } + + /// @return height of the tournament[level] tree which is calculated by subtracting the log2step[level] from the log2step[level - 1] + function height(uint64 level) internal pure returns (uint64) { + uint64[LEVELS] memory arr = [ + uint64(32), + uint64(15), + uint64(16) + ]; + return arr[level]; + } +} diff --git a/onchain/permissionless-arbitration/src/Clock.sol b/onchain/permissionless-arbitration/src/Clock.sol new file mode 100644 index 000000000..0eef80c15 --- /dev/null +++ b/onchain/permissionless-arbitration/src/Clock.sol @@ -0,0 +1,159 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./Time.sol"; +import "./CanonicalConstants.sol"; + +library Clock { + using Time for Time.Instant; + using Time for Time.Duration; + + using Clock for State; + + struct State { + Time.Duration allowance; + Time.Instant startInstant; // the timestamp when the clock started ticking, zero means clock is paused + } + + // + // View/Pure methods + // + + function notInitialized(State memory state) internal pure returns (bool) { + return state.allowance.isZero(); + } + + function requireInitialized(State memory state) internal pure { + require(!state.notInitialized(), "clock is not initialized"); + } + + function requireNotInitialized(State memory state) internal pure { + require(state.notInitialized(), "clock is initialized"); + } + + function hasTimeLeft(State memory state) internal view returns (bool) { + if (state.startInstant.isZero()) { + return true; + } else { + return + state.allowance.gt( + Time.timeSpan(Time.currentTime(), state.startInstant) + ); + } + } + + /// @return deadline of the two clocks should be the tolerances combined + function deadline( + State memory freshState1, + State memory freshState2 + ) internal view returns (Time.Instant) { + Time.Duration duration = freshState1.allowance.add( + freshState2.allowance + ); + return Time.currentTime().add(duration); + } + + /// @return max tolerance of the two clocks + function max( + State memory pausedState1, + State memory pausedState2 + ) internal pure returns (Time.Duration) { + if (pausedState1.allowance.gt(pausedState2.allowance)) { + return pausedState1.allowance; + } else { + return pausedState2.allowance; + } + } + + /// @return duration of time has elapsed since the clock timeout + function timeSinceTimeout( + State memory state + ) internal view returns (Time.Duration) { + return + Time.timeSpan(Time.currentTime(), state.startInstant).monus( + state.allowance + ); + } + + // + // Storage methods + // + + function setNewPaused( + State storage state, + Time.Instant checkinInstant, + Time.Duration initialAllowance + ) internal { + Time.Duration allowance = initialAllowance.monus( + Time.currentTime().timeSpan(checkinInstant) + ); + + if (allowance.isZero()) { + revert("can't create clock with zero time"); + } + + state.allowance = allowance; + state.startInstant = Time.ZERO_INSTANT; + } + + /// @notice Resume the clock from pause state, or pause a clock and update the allowance + function advanceClock(State storage state) internal { + Time.Duration _timeLeft = timeLeft(state); + + if (_timeLeft.isZero()) { + revert("can't advance clock with no time left"); + } + + toggleClock(state); + state.allowance = _timeLeft; + } + + function addValidatorEffort(State storage state, Time.Duration deduction) internal { + Time.Duration _timeLeft = state.allowance.monus( + deduction + ); + + if (_timeLeft.isZero()) { + revert("can't reset clock with no time left"); + } + + Time.Duration _allowance = _timeLeft.add(ArbitrationConstants.VALIDATOR_EFFORT); + if (_allowance.gt(ArbitrationConstants.DISPUTE_TIMEOUT)) { + _allowance = ArbitrationConstants.DISPUTE_TIMEOUT; + } + + state.allowance = _allowance; + state.startInstant = Time.ZERO_INSTANT; + } + + function setPaused(State storage state) internal { + if (!state.startInstant.isZero()) { + state.advanceClock(); + } + } + + // + // Private + // + + function timeLeft(State memory state) private view returns (Time.Duration) { + if (state.startInstant.isZero()) { + return state.allowance; + } else { + return + state.allowance.monus( + Time.timeSpan(Time.currentTime(), state.startInstant) + ); + } + } + + function toggleClock(State storage state) private { + if (state.startInstant.isZero()) { + state.startInstant = Time.currentTime(); + } else { + state.startInstant = Time.ZERO_INSTANT; + } + } +} diff --git a/onchain/permissionless-arbitration/src/Commitment.sol b/onchain/permissionless-arbitration/src/Commitment.sol new file mode 100644 index 000000000..e7edc4859 --- /dev/null +++ b/onchain/permissionless-arbitration/src/Commitment.sol @@ -0,0 +1,92 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./CanonicalConstants.sol"; +import "./Tree.sol"; +import "./Machine.sol"; + +// import "./Merkle.sol"; + +library Commitment { + using Tree for Tree.Node; + using Commitment for Tree.Node; + + function requireState( + Tree.Node commitment, + uint64 level, + uint256 position, + Machine.Hash state, + bytes32[] calldata hashProof + ) internal pure { + uint64 treeHeight = ArbitrationConstants.height(level); + Tree.Node expectedCommitment = getRoot( + Machine.Hash.unwrap(state), + treeHeight, + position, + hashProof + ); + + require(commitment.eq(expectedCommitment), "commitment state doesn't match"); + } + + + function isEven(uint256 x) private pure returns (bool) { + return x % 2 == 0; + } + + function getRoot( + bytes32 leaf, + uint64 treeHeight, + uint256 position, + bytes32[] calldata siblings + ) internal pure returns (Tree.Node) { + uint nodesCount = treeHeight - 1; + assert(nodesCount == siblings.length); + + for (uint i = 0; i < nodesCount; i++) { + if (isEven(position >> i)) { + leaf = + keccak256(abi.encodePacked(leaf, siblings[i])); + } else { + leaf = + keccak256(abi.encodePacked(siblings[i], leaf)); + } + } + + return Tree.Node.wrap(leaf); + } + + + function requireFinalState( + Tree.Node commitment, + uint64 level, + Machine.Hash finalState, + bytes32[] calldata hashProof + ) internal pure { + uint64 treeHeight = ArbitrationConstants.height(level); + Tree.Node expectedCommitment = getRootForLastLeaf( + treeHeight, + Machine.Hash.unwrap(finalState), + hashProof + ); + + require(commitment.eq(expectedCommitment), "commitment last state doesn't match"); + } + + + function getRootForLastLeaf( + uint64 treeHeight, + bytes32 leaf, + bytes32[] calldata siblings + ) internal pure returns (Tree.Node) { + assert(treeHeight == siblings.length); + + for (uint i = 0; i < treeHeight; i++) { + leaf = keccak256(abi.encodePacked(siblings[i], leaf)); + } + + return Tree.Node.wrap(leaf); + } +} diff --git a/onchain/permissionless-arbitration/src/Machine.sol b/onchain/permissionless-arbitration/src/Machine.sol new file mode 100644 index 000000000..f6a97758e --- /dev/null +++ b/onchain/permissionless-arbitration/src/Machine.sol @@ -0,0 +1,24 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +library Machine { + type Hash is bytes32; + + Hash constant ZERO_STATE = Hash.wrap(0x0); + + function notInitialized(Hash hash) internal pure returns (bool) { + bytes32 h = Hash.unwrap(hash); + return h == 0x0; + } + + function eq(Hash left, Hash right) internal pure returns (bool) { + bytes32 l = Hash.unwrap(left); + bytes32 r = Hash.unwrap(right); + return l == r; + } + + type Cycle is uint256; // TODO overcomplicated? + type Log2Step is uint64; // TODO overcomplicated? +} diff --git a/onchain/permissionless-arbitration/src/Match.sol b/onchain/permissionless-arbitration/src/Match.sol new file mode 100644 index 000000000..17976d185 --- /dev/null +++ b/onchain/permissionless-arbitration/src/Match.sol @@ -0,0 +1,368 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./CanonicalConstants.sol"; +import "./Tree.sol"; +import "./Machine.sol"; +import "./Commitment.sol"; + +/// @notice Implements functionalities to advance a match, until the point where divergence is found. +library Match { + using Tree for Tree.Node; + using Match for Id; + using Match for IdHash; + using Match for State; + using Machine for Machine.Hash; + using Commitment for Tree.Node; + + // + // Events + // + + event matchAdvanced(Match.IdHash indexed, Tree.Node parent, Tree.Node left); + + + // + // Id + // + + struct Id { + Tree.Node commitmentOne; + Tree.Node commitmentTwo; + } + + // + // IdHash + // + type IdHash is bytes32; + + IdHash constant ZERO_ID = IdHash.wrap(bytes32(0x0)); + + function hashFromId(Id memory id) internal pure returns (IdHash) { + return IdHash.wrap(keccak256(abi.encode(id))); + } + + function isZero(IdHash idHash) internal pure returns (bool) { + return IdHash.unwrap(idHash) == 0x0; + } + + function eq(IdHash left, IdHash right) internal pure returns (bool) { + bytes32 l = IdHash.unwrap(left); + bytes32 r = IdHash.unwrap(right); + return l == r; + } + + function requireEq(IdHash left, IdHash right) internal pure { + require(left.eq(right), "matches are not equal"); + } + + function requireExist(IdHash idHash) internal pure { + require(!idHash.isZero(), "match doesn't exist"); + } + + // + // State + // + + struct State { + Tree.Node otherParent; + Tree.Node leftNode; + Tree.Node rightNode; + // Once match is done, leftNode and rightNode change meaning + // and contains contested final states. + uint256 runningLeafPosition; + uint64 currentHeight; + + uint64 level; // constant + } + + function createMatch( + Tree.Node one, + Tree.Node two, + Tree.Node leftNodeOfTwo, + Tree.Node rightNodeOfTwo, + uint64 level + ) internal pure returns (IdHash, State memory) { + assert(two.verify(leftNodeOfTwo, rightNodeOfTwo)); + + Id memory matchId = Id(one, two); + + State memory state = State( + one, + leftNodeOfTwo, + rightNodeOfTwo, + 0, + ArbitrationConstants.height(level), + level + ); + + return (matchId.hashFromId(), state); + } + + function advanceMatch( + State storage state, + Id calldata id, + Tree.Node leftNode, + Tree.Node rightNode, + Tree.Node newLeftNode, + Tree.Node newRightNode + ) internal { + if (!state.agreesOnLeftNode(leftNode)) { + // go down left in Commitment tree + leftNode.requireChildren(newLeftNode, newRightNode); + state._goDownLeftTree(newLeftNode, newRightNode); + } else { + // go down right in Commitment tree + rightNode.requireChildren(newLeftNode, newRightNode); + state._goDownRightTree(newLeftNode, newRightNode); + } + + emit matchAdvanced( + id.hashFromId(), + state.otherParent, + state.leftNode + ); + } + + function sealMatch( + State storage state, + Id calldata id, + Machine.Hash initialState, + Tree.Node leftLeaf, + Tree.Node rightLeaf, + Machine.Hash agreeState, + bytes32[] calldata agreeStateProof + ) + internal + returns ( + Machine.Hash divergentStateOne, + Machine.Hash divergentStateTwo + ) + { + if (!state.agreesOnLeftNode(leftLeaf)) { + // Divergence is in the left leaf! + (divergentStateOne, divergentStateTwo) = state + ._setDivergenceOnLeftLeaf(leftLeaf); + } else { + // Divergence is in the right leaf! + (divergentStateOne, divergentStateTwo) = state + ._setDivergenceOnRightLeaf(rightLeaf); + } + + // Prove initial hash is in commitment + if (state.runningLeafPosition == 0) { + require(agreeState.eq(initialState), "agree hash incorrect"); + } else { + id.commitmentOne.requireState( + state.level, + state.runningLeafPosition - 1, + agreeState, + agreeStateProof + ); + } + + state._setAgreeState(agreeState); + } + + + // + // View methods + // + + function exists(State memory state) internal pure returns (bool) { + return !state.otherParent.isZero(); + } + + function isFinished(State memory state) internal pure returns (bool) { + return state.currentHeight == 0; + } + + function canBeFinalized(State memory state) internal pure returns (bool) { + return state.currentHeight == 1; + } + + function canBeAdvanced(State memory state) internal pure returns (bool) { + return state.currentHeight > 1; + } + + function agreesOnLeftNode( + State memory state, + Tree.Node newLeftNode + ) internal pure returns (bool) { + return newLeftNode.eq(state.leftNode); + } + + function toCycle( + State memory state, + uint256 startCycle + ) internal pure returns (uint256) { + uint64 log2step = ArbitrationConstants.log2step(state.level); + return _toCycle(state, startCycle, log2step); + } + + function height( + State memory state + ) internal pure returns (uint64) { + return ArbitrationConstants.height(state.level); + } + + function getDivergence( + State memory state, + uint256 startCycle + ) + internal + pure + returns ( + Machine.Hash agreeHash, + uint256 agreeCycle, + Machine.Hash finalStateOne, + Machine.Hash finalStateTwo + ) + { + assert(state.currentHeight == 0); + agreeHash = Machine.Hash.wrap(Tree.Node.unwrap(state.otherParent)); + agreeCycle = state.toCycle(startCycle); + + if (state.runningLeafPosition % 2 == 0) { + // divergence was set on left leaf + if (state.height() % 2 == 0) { + finalStateOne = state.leftNode.toMachineHash(); + finalStateTwo = state.rightNode.toMachineHash(); + } else { + finalStateOne = state.rightNode.toMachineHash(); + finalStateTwo = state.leftNode.toMachineHash(); + } + } else { + // divergence was set on right leaf + if (state.height() % 2 == 0) { + finalStateOne = state.rightNode.toMachineHash(); + finalStateTwo = state.leftNode.toMachineHash(); + } else { + finalStateOne = state.leftNode.toMachineHash(); + finalStateTwo = state.rightNode.toMachineHash(); + } + } + } + + + // + // Requires + // + + function requireExist(State memory state) internal pure { + require(state.exists(), "match does not exist"); + } + + function requireIsFinished(State memory state) internal pure { + require(state.isFinished(), "match is not finished"); + } + + function requireCanBeFinalized(State memory state) internal pure { + require(state.canBeFinalized(), "match is not ready to be finalized"); + } + + function requireCanBeAdvanced(State memory state) internal pure { + require(state.canBeAdvanced(), "match can't be advanced"); + } + + function requireParentHasChildren( + State memory state, + Tree.Node leftNode, + Tree.Node rightNode + ) internal pure { + state.otherParent.requireChildren(leftNode, rightNode); + } + + + // + // Private + // + + function _goDownLeftTree( + State storage state, + Tree.Node newLeftNode, + Tree.Node newRightNode + ) internal { + assert(state.currentHeight > 1); + state.otherParent = state.leftNode; + state.leftNode = newLeftNode; + state.rightNode = newRightNode; + + state.currentHeight--; + } + + function _goDownRightTree( + State storage state, + Tree.Node newLeftNode, + Tree.Node newRightNode + ) internal { + assert(state.currentHeight > 1); + state.otherParent = state.rightNode; + state.leftNode = newLeftNode; + state.rightNode = newRightNode; + + state.currentHeight--; + state.runningLeafPosition += 1 << state.currentHeight; + } + + function _setDivergenceOnLeftLeaf( + State storage state, + Tree.Node leftLeaf + ) + internal + returns (Machine.Hash finalStateOne, Machine.Hash finalStateTwo) + { + assert(state.currentHeight == 1); + state.rightNode = leftLeaf; + state.currentHeight = 0; + + if (state.height() % 2 == 0) { + finalStateOne = state.leftNode.toMachineHash(); + finalStateTwo = state.rightNode.toMachineHash(); + } else { + finalStateOne = state.rightNode.toMachineHash(); + finalStateTwo = state.leftNode.toMachineHash(); + } + } + + function _setDivergenceOnRightLeaf( + State storage state, + Tree.Node rightLeaf + ) + internal + returns (Machine.Hash finalStateOne, Machine.Hash finalStateTwo) + { + assert(state.currentHeight == 1); + state.leftNode = rightLeaf; + state.currentHeight = 0; + state.runningLeafPosition += 1; + + if (state.height() % 2 == 0) { + finalStateOne = state.rightNode.toMachineHash(); + finalStateTwo = state.leftNode.toMachineHash(); + } else { + finalStateOne = state.leftNode.toMachineHash(); + finalStateTwo = state.rightNode.toMachineHash(); + } + } + + function _setAgreeState( + State storage state, + Machine.Hash initialState + ) internal { + assert(state.currentHeight == 0); + state.otherParent = Tree.Node.wrap(Machine.Hash.unwrap(initialState)); + } + + function _toCycle( + State memory state, + uint256 base, + uint64 log2step + ) internal pure returns (uint256) { + uint256 step = 1 << log2step; + uint256 leafPosition = state.runningLeafPosition; + return base + (leafPosition * step); + } +} diff --git a/onchain/permissionless-arbitration/src/Merkle.sol b/onchain/permissionless-arbitration/src/Merkle.sol new file mode 100644 index 000000000..1cfd1a813 --- /dev/null +++ b/onchain/permissionless-arbitration/src/Merkle.sol @@ -0,0 +1,52 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +library Merkle { + function getRootWithValue( + uint64 _position, + bytes8 _value, + bytes32[] memory _proof + ) public pure returns (bytes32) { + bytes32 _runningHash = keccak256(abi.encodePacked(_value)); + + return getRootWithDrive(_position, 3, _runningHash, _proof); + } + + function getRootWithHash( + uint64 _position, + bytes32 _hash, + bytes32[] memory _proof + ) public pure returns (bytes32) { + return getRootWithDrive(_position, 3, _hash, _proof); + } + + function getRootWithDrive( + uint64 _position, + uint8 _logOfSize, + bytes32 _drive, + bytes32[] memory _siblings + ) public pure returns (bytes32) { + require(_logOfSize >= 3, "Must be at least a word"); + require(_logOfSize <= 64, "Cannot be bigger than the machine itself"); + + uint64 _size = uint64(2) ** _logOfSize; + + require(((_size - 1) & _position) == 0, "Position is not aligned"); + require( + _siblings.length == 64 - _logOfSize, + "Proof length does not match" + ); + + for (uint64 _i = 0; _i < _siblings.length; _i++) { + if ((_position & (_size << _i)) == 0) { + _drive = keccak256(abi.encodePacked(_drive, _siblings[_i])); + } else { + _drive = keccak256(abi.encodePacked(_siblings[_i], _drive)); + } + } + + return _drive; + } +} diff --git a/onchain/permissionless-arbitration/src/Time.sol b/onchain/permissionless-arbitration/src/Time.sol new file mode 100644 index 000000000..40cd1ae34 --- /dev/null +++ b/onchain/permissionless-arbitration/src/Time.sol @@ -0,0 +1,101 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +library Time { + type Instant is uint64; + type Duration is uint64; + + using Time for Instant; // TODO rename to Instant + using Time for Duration; + + Instant constant ZERO_INSTANT = Instant.wrap(0); + Duration constant ZERO_DURATION = Duration.wrap(0); + + function currentTime() internal view returns (Instant) { + return Instant.wrap(uint64(block.timestamp)); + } + + function add( + Instant timestamp, + Duration duration + ) internal pure returns (Instant) { + uint64 t = Instant.unwrap(timestamp); + uint64 d = Duration.unwrap(duration); + return Instant.wrap(t + d); + } + + function gt(Instant left, Instant right) internal pure returns (bool) { + uint64 l = Instant.unwrap(left); + uint64 r = Instant.unwrap(right); + return l > r; + } + + function gt(Duration left, Duration right) internal pure returns (bool) { + uint64 l = Duration.unwrap(left); + uint64 r = Duration.unwrap(right); + return l > r; + } + + function isZero(Instant timestamp) internal pure returns (bool) { + uint64 t = Instant.unwrap(timestamp); + return t == 0; + } + + function isZero(Duration duration) internal pure returns (bool) { + uint64 d = Duration.unwrap(duration); + return d == 0; + } + + function add( + Duration left, + Duration right + ) internal pure returns (Duration) { + uint64 l = Duration.unwrap(left); + uint64 r = Duration.unwrap(right); + return Duration.wrap(l + r); + } + + function sub( + Duration left, + Duration right + ) internal pure returns (Duration) { + uint64 l = Duration.unwrap(left); + uint64 r = Duration.unwrap(right); + return Duration.wrap(l - r); + } + + function monus( + Duration left, + Duration right + ) internal pure returns (Duration) { + uint64 l = Duration.unwrap(left); + uint64 r = Duration.unwrap(right); + return Duration.wrap(l < r ? 0 : l - r); + } + + function timeSpan( + Instant left, + Instant right + ) internal pure returns (Duration) { + uint64 l = Instant.unwrap(left); + uint64 r = Instant.unwrap(right); + return Duration.wrap(l - r); + } + + function timeoutElapsedSince( + Instant timestamp, + Duration duration, + Instant current + ) internal pure returns (bool) { + return !timestamp.add(duration).gt(current); + } + + function timeoutElapsed( + Instant timestamp, + Duration duration + ) internal view returns (bool) { + return timestamp.timeoutElapsedSince(duration, currentTime()); + } +} diff --git a/onchain/permissionless-arbitration/src/Tree.sol b/onchain/permissionless-arbitration/src/Tree.sol new file mode 100644 index 000000000..5bfdfc6d9 --- /dev/null +++ b/onchain/permissionless-arbitration/src/Tree.sol @@ -0,0 +1,52 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./Machine.sol"; + +library Tree { + using Tree for Node; + + type Node is bytes32; + + Node constant ZERO_NODE = Node.wrap(bytes32(0x0)); + + function eq(Node left, Node right) internal pure returns (bool) { + bytes32 l = Node.unwrap(left); + bytes32 r = Node.unwrap(right); + return l == r; + } + + function join(Node left, Node right) internal pure returns (Node) { + bytes32 l = Node.unwrap(left); + bytes32 r = Node.unwrap(right); + bytes32 p = keccak256(abi.encode(l, r)); + return Node.wrap(p); + } + + function verify( + Node parent, + Node left, + Node right + ) internal pure returns (bool) { + return parent.eq(left.join(right)); + } + + function requireChildren(Node parent, Node left, Node right) internal pure { + require(parent.verify(left, right), "child nodes don't match parent"); + } + + function isZero(Node node) internal pure returns (bool) { + bytes32 n = Node.unwrap(node); + return n == 0x0; + } + + function requireExist(Node node) internal pure { + require(!node.isZero(), "tree node doesn't exist"); + } + + function toMachineHash(Node node) internal pure returns (Machine.Hash) { + return Machine.Hash.wrap(Node.unwrap(node)); + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/abstracts/LeafTournament.sol b/onchain/permissionless-arbitration/src/tournament/abstracts/LeafTournament.sol new file mode 100644 index 000000000..c336ef889 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/abstracts/LeafTournament.sol @@ -0,0 +1,150 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./Tournament.sol"; +import "../../Commitment.sol"; + +import "step/ready_src/UArchStep.sol"; + +/// @notice Leaf tournament is the one that seals leaf match +abstract contract LeafTournament is Tournament { + using Machine for Machine.Hash; + using Commitment for Tree.Node; + using Tree for Tree.Node; + using Clock for Clock.State; + using Match for Match.Id; + using Match for Match.State; + + constructor() {} + + function sealLeafMatch( + Match.Id calldata _matchId, + Tree.Node _leftLeaf, + Tree.Node _rightLeaf, + Machine.Hash _agreeHash, + bytes32[] calldata _agreeHashProof + ) external tournamentNotFinished { + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireExist(); + _matchState.requireCanBeFinalized(); + _matchState.requireParentHasChildren(_leftLeaf, _rightLeaf); + + // Unpause clocks + { + Clock.State storage _clock1 = clocks[_matchId.commitmentOne]; + Clock.State storage _clock2 = clocks[_matchId.commitmentTwo]; + _clock1.setPaused(); + _clock1.advanceClock(); + _clock2.setPaused(); + _clock2.advanceClock(); + } + + _matchState.sealMatch( + _matchId, + initialHash, + _leftLeaf, + _rightLeaf, + _agreeHash, + _agreeHashProof + ); + } + + function winLeafMatch( + Match.Id calldata _matchId, + Tree.Node _leftNode, + Tree.Node _rightNode, + bytes calldata proofs + ) external tournamentNotFinished { + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireExist(); + _matchState.requireIsFinished(); + + Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; + Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; + _clockOne.requireInitialized(); + _clockTwo.requireInitialized(); + + ( + Machine.Hash _agreeHash, + uint256 _agreeCycle, + Machine.Hash _finalStateOne, + Machine.Hash _finalStateTwo + ) = _matchState.getDivergence(startCycle); + + Machine.Hash _finalState = runMetaStep( + _agreeHash, + _agreeCycle, + proofs + ); + + if (_leftNode.join(_rightNode).eq(_matchId.commitmentOne)) { + require( + _finalState.eq(_finalStateOne), + "final state one doesn't match" + ); + + _clockOne.addValidatorEffort(Time.ZERO_DURATION); + pairCommitment( + _matchId.commitmentOne, + _clockOne, + _leftNode, + _rightNode + ); + } else if (_leftNode.join(_rightNode).eq(_matchId.commitmentTwo)) { + require( + _finalState.eq(_finalStateTwo), + "final state two doesn't match" + ); + + _clockTwo.addValidatorEffort(Time.ZERO_DURATION); + pairCommitment( + _matchId.commitmentTwo, + _clockTwo, + _leftNode, + _rightNode + ); + } else { + revert("wrong left/right nodes for step"); + } + + delete matches[_matchId.hashFromId()]; + } + + function runMetaStep(Machine.Hash machineState, uint256 counter, bytes memory proofs) + internal + pure + returns (Machine.Hash) + { + return Machine.Hash.wrap(metaStep( + Machine.Hash.unwrap(machineState), + counter, + proofs + )); + } + + // TODO: move to step repo + // TODO: add ureset + function metaStep(bytes32 machineState, uint256 counter, bytes memory proofs) + internal + pure + returns (bytes32) + { + // TODO: create a more convinient constructor. + AccessLogs.Context memory accessLogs = AccessLogs.Context( + machineState, + Buffer.Context(proofs, 0) + ); + + uint256 mask = (1 << 64) - 1; + if (counter & mask == mask) { + // reset + revert("RESET UNIMPLEMENTED"); + } else { + UArchStep.step(accessLogs); + bytes32 newMachineState = accessLogs.currentRootHash; + return newMachineState; + } + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/abstracts/NonLeafTournament.sol b/onchain/permissionless-arbitration/src/tournament/abstracts/NonLeafTournament.sol new file mode 100644 index 000000000..6e095585d --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/abstracts/NonLeafTournament.sol @@ -0,0 +1,194 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../factories/ITournamentFactory.sol"; +import "./Tournament.sol"; +import "./NonRootTournament.sol"; + +/// @notice Non-leaf tournament can create inner tournaments and matches +abstract contract NonLeafTournament is Tournament { + using Clock for Clock.State; + using Commitment for Tree.Node; + using Machine for Machine.Hash; + using Tree for Tree.Node; + using Time for Time.Instant; + using Match for Match.State; + using Match for Match.Id; + using Match for Match.IdHash; + + // + // Constants + // + + ITournamentFactory immutable tournamentFactory; + + // + // Storage + // + mapping(NonRootTournament => Match.IdHash) matchIdFromInnerTournaments; + + // + // Events + // + + event newInnerTournament(Match.IdHash indexed, NonRootTournament); + + // + // Modifiers + // + + modifier onlyInnerTournament() { + Match.IdHash matchIdHash = matchIdFromInnerTournaments[ + NonRootTournament(msg.sender) + ]; + matches[matchIdHash].requireExist(); + _; + } + + // + // Constructor + // + + constructor(ITournamentFactory _tournamentFactory) { + tournamentFactory = _tournamentFactory; + } + + function sealInnerMatchAndCreateInnerTournament( + Match.Id calldata _matchId, + Tree.Node _leftLeaf, + Tree.Node _rightLeaf, + Machine.Hash _agreeHash, + bytes32[] calldata _agreeHashProof + ) external tournamentNotFinished { + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireCanBeFinalized(); + _matchState.requireParentHasChildren(_leftLeaf, _rightLeaf); + + // Pause clocks + Time.Duration _maxDuration; + { + Clock.State storage _clock1 = clocks[_matchId.commitmentOne]; + Clock.State storage _clock2 = clocks[_matchId.commitmentTwo]; + _clock1.setPaused(); + _clock2.setPaused(); + _maxDuration = Clock.max(_clock1, _clock2); + } + + ( + Machine.Hash _finalStateOne, + Machine.Hash _finalStateTwo + ) = _matchState.sealMatch( + _matchId, + initialHash, + _leftLeaf, + _rightLeaf, + _agreeHash, + _agreeHashProof + ); + + NonRootTournament _inner = instantiateInner( + _agreeHash, + _matchId.commitmentOne, + _finalStateOne, + _matchId.commitmentTwo, + _finalStateTwo, + _maxDuration, + _matchState.toCycle(startCycle), + level + 1 + ); + matchIdFromInnerTournaments[_inner] = _matchId.hashFromId(); + + emit newInnerTournament(_matchId.hashFromId(), _inner); + } + + function winInnerMatch( + NonRootTournament _childTournament, + Tree.Node _leftNode, + Tree.Node _rightNode + ) external tournamentNotFinished { + Match.IdHash _matchIdHash = matchIdFromInnerTournaments[_childTournament]; + _matchIdHash.requireExist(); + + Match.State storage _matchState = matches[_matchIdHash]; + _matchState.requireExist(); + _matchState.requireIsFinished(); + + (bool finished, Tree.Node _winner) = _childTournament.innerTournamentWinner(); + require(finished, "child tournament is not finished"); + _winner.requireExist(); + + Tree.Node _commitmentRoot = _leftNode.join(_rightNode); + require(_commitmentRoot.eq(_winner), "tournament winner is different"); + + Clock.State storage _clock = clocks[_commitmentRoot]; + _clock.requireInitialized(); + _clock.addValidatorEffort( + Time + .currentTime() + .timeSpan(_childTournament.maximumEnforceableDelay()) + ); + + pairCommitment( + _commitmentRoot, + _clock, + _leftNode, + _rightNode + ); + + // delete storage + delete matches[_matchIdHash]; + matchIdFromInnerTournaments[_childTournament] = Match.ZERO_ID; + } + + + function updateTournamentDelay( + Time.Instant _delay + ) external onlyInnerTournament { + bool overrode = setMaximumDelay(_delay); + if (overrode) { + updateParentTournamentDelay(_delay); + } + } + + function instantiateInner( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level + ) private returns (NonRootTournament) { + // the inner tournament is bottom tournament at last level + // else instantiate middle tournament + Tournament _tournament; + if (_level == ArbitrationConstants.LEVELS - 1) { + _tournament = tournamentFactory.instantiateBottom( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level + ); + } else { + _tournament = tournamentFactory.instantiateMiddle( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level + ); + } + + return NonRootTournament(address(_tournament)); + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/abstracts/NonRootTournament.sol b/onchain/permissionless-arbitration/src/tournament/abstracts/NonRootTournament.sol new file mode 100644 index 000000000..744887e8e --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/abstracts/NonRootTournament.sol @@ -0,0 +1,81 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./Tournament.sol"; +import "./NonLeafTournament.sol"; + +/// @notice Non-root tournament needs to propagate side-effects to its parent +abstract contract NonRootTournament is Tournament { + using Machine for Machine.Hash; + using Tree for Tree.Node; + + // + // Constants + // + NonLeafTournament immutable parentTournament; + + Tree.Node immutable contestedCommitmentOne; + Machine.Hash immutable contestedFinalStateOne; + Tree.Node immutable contestedCommitmentTwo; + Machine.Hash immutable contestedFinalStateTwo; + + // + // Constructor + // + + constructor( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level, + NonLeafTournament _parent + ) Tournament(_initialHash, _allowance, _startCycle, _level) { + parentTournament = _parent; + + contestedCommitmentOne = _contestedCommitmentOne; + contestedFinalStateOne = _contestedFinalStateOne; + contestedCommitmentTwo = _contestedCommitmentTwo; + contestedFinalStateTwo = _contestedFinalStateTwo; + } + + /// @notice get the dangling commitment at current level and then retrieve the winner commitment + function innerTournamentWinner() external view returns (bool, Tree.Node) { + if (!isFinished()) { + return (false, Tree.ZERO_NODE); + } + + ( + bool _hasDanglingCommitment, + Tree.Node _danglingCommitment + ) = hasDanglingCommitment(); + assert(_hasDanglingCommitment); + + Machine.Hash _finalState = finalStates[_danglingCommitment]; + + if (_finalState.eq(contestedFinalStateOne)) { + return (true, contestedCommitmentOne); + } else { + assert(_finalState.eq(contestedFinalStateTwo)); + return (true, contestedCommitmentTwo); + } + } + + function updateParentTournamentDelay( + Time.Instant _delay + ) internal override { + parentTournament.updateTournamentDelay(_delay); + } + + /// @notice a final state is valid if it's equal to ContestedFinalStateOne or ContestedFinalStateTwo + function validContestedFinalState( + Machine.Hash _finalState + ) internal view override returns (bool) { + return contestedFinalStateOne.eq(_finalState) || contestedFinalStateTwo.eq(_finalState); + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/abstracts/RootTournament.sol b/onchain/permissionless-arbitration/src/tournament/abstracts/RootTournament.sol new file mode 100644 index 000000000..ae7a48b95 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/abstracts/RootTournament.sol @@ -0,0 +1,43 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./Tournament.sol"; + +/// @notice Root tournament has no parent +abstract contract RootTournament is Tournament { + // + // Constructor + // + + constructor( + Machine.Hash _initialHash + ) Tournament(_initialHash, ArbitrationConstants.CENSORSHIP_TOLERANCE, 0, 0) {} + + function updateParentTournamentDelay( + Time.Instant _delay + ) internal override { + // do nothing, the root tournament has no parent to update + } + + function validContestedFinalState( + Machine.Hash + ) internal pure override returns (bool) { + // always returns true in root tournament + return true; + } + + function arbitrationResult() external view returns (bool, Tree.Node, Machine.Hash) { + if (!isFinished()) { + return (false, Tree.ZERO_NODE, Machine.ZERO_STATE); + } + + (bool _hasDanglingCommitment, Tree.Node _danglingCommitment) = + hasDanglingCommitment(); + assert(_hasDanglingCommitment); + + Machine.Hash _finalState = finalStates[_danglingCommitment]; + return (true, _danglingCommitment, _finalState); + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/abstracts/Tournament.sol b/onchain/permissionless-arbitration/src/tournament/abstracts/Tournament.sol new file mode 100644 index 000000000..3874fe0e2 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/abstracts/Tournament.sol @@ -0,0 +1,376 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../../CanonicalConstants.sol"; + +import "../../Commitment.sol"; +import "../../Time.sol"; +import "../../Machine.sol"; +import "../../Tree.sol"; +import "../../Clock.sol"; +import "../../Match.sol"; + +/// @notice Implements the core functionalities of a permissionless tournament that resolves +/// disputes of n parties in O(log(n)) +/// @dev tournaments and matches are nested alternately. Anyone can join a tournament +/// while the tournament is still open, and two of the participants with unique commitments +/// will form a match. A match located in the last level is called `leafMatch`, +/// meaning the one-step disagreement is found and can be resolved by solidity-step. +/// Non-leaf (inner) matches would normally create inner tournaments with height = height + 1, +/// to find the divergence with improved precision. +abstract contract Tournament { + using Machine for Machine.Hash; + using Tree for Tree.Node; + using Commitment for Tree.Node; + + using Time for Time.Instant; + using Time for Time.Duration; + + using Clock for Clock.State; + + using Match for Match.Id; + using Match for Match.IdHash; + using Match for Match.State; + + // + // Constants + // + + Machine.Hash immutable initialHash; + + uint256 immutable startCycle; + uint64 immutable level; + + Time.Instant immutable startInstant; + Time.Duration immutable allowance; + + // + // Storage + // + + Time.Instant public maximumEnforceableDelay; + Tree.Node danglingCommitment; + + mapping(Tree.Node => Clock.State) clocks; + mapping(Tree.Node => Machine.Hash) finalStates; + // matches existing in current tournament + mapping(Match.IdHash => Match.State) matches; + + // + // Events + // + + event matchCreated( + Tree.Node indexed one, + Tree.Node indexed two, + Tree.Node leftOfTwo + ); + event commitmentJoined(Tree.Node root); + + + // + // Modifiers + // + + modifier tournamentNotFinished() { + require(!isFinished(), "tournament is finished"); + + _; + } + + modifier tournamentOpen() { + require(!isClosed(), "tournament check-in elapsed"); + + _; + } + + // + // Constructor + // + + constructor( + Machine.Hash _initialHash, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level + ) { + initialHash = _initialHash; + startCycle = _startCycle; + level = _level; + startInstant = Time.currentTime(); + allowance = _allowance; + + if (_allowance.gt(ArbitrationConstants.CENSORSHIP_TOLERANCE)) { + maximumEnforceableDelay = Time.currentTime().add( + ArbitrationConstants.CENSORSHIP_TOLERANCE + ); + } else { + maximumEnforceableDelay = Time.currentTime().add(_allowance); + } + } + + + // + // Virtual Methods + // + + function updateParentTournamentDelay(Time.Instant _delay) internal virtual; + + /// @return bool if commitment with _finalState is allowed to join the tournament + function validContestedFinalState( + Machine.Hash _finalState + ) internal view virtual returns (bool); + + + // + // Methods + // + + /// @dev root tournaments are open to everyone, while non-root tournaments are open to anyone who's final state hash matches the one of the two in the tournament + function joinTournament( + Machine.Hash _finalState, + bytes32[] calldata _proof, + Tree.Node _leftNode, + Tree.Node _rightNode + ) external tournamentOpen { + Tree.Node _commitmentRoot = _leftNode.join(_rightNode); + + // Prove final state is in commitmentRoot + _commitmentRoot.requireFinalState(level, _finalState, _proof); + + // Verify whether finalState is one of the two allowed of tournament if nested + requireValidContestedFinalState(_finalState); + finalStates[_commitmentRoot] = _finalState; + + Clock.State storage _clock = clocks[_commitmentRoot]; + _clock.requireNotInitialized(); // reverts if commitment is duplicate + _clock.setNewPaused(startInstant, allowance); + + pairCommitment(_commitmentRoot, _clock, _leftNode, _rightNode); + emit commitmentJoined(_commitmentRoot); + } + + /// @notice Advance the match until the smallest divergence is found at current level + /// @dev this function is being called repeatedly in turns by the two parties that disagree on the commitment. + function advanceMatch( + Match.Id calldata _matchId, + Tree.Node _leftNode, + Tree.Node _rightNode, + Tree.Node _newLeftNode, + Tree.Node _newRightNode + ) external tournamentNotFinished { + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireExist(); + _matchState.requireCanBeAdvanced(); + _matchState.requireParentHasChildren(_leftNode, _rightNode); + + _matchState.advanceMatch( + _matchId, + _leftNode, + _rightNode, + _newLeftNode, + _newRightNode + ); + + // advance clocks + clocks[_matchId.commitmentOne].advanceClock(); + clocks[_matchId.commitmentTwo].advanceClock(); + } + + function winMatchByTimeout( + Match.Id calldata _matchId, + Tree.Node _leftNode, + Tree.Node _rightNode + ) external tournamentNotFinished { + matches[_matchId.hashFromId()].requireExist(); + Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; + Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; + + _clockOne.requireInitialized(); + _clockTwo.requireInitialized(); + + if (_clockOne.hasTimeLeft() && !_clockTwo.hasTimeLeft()) { + require( + _matchId.commitmentOne.verify(_leftNode, _rightNode), + "child nodes do not match parent (commitmentOne)" + ); + + _clockOne.addValidatorEffort(_clockTwo.timeSinceTimeout()); + pairCommitment( + _matchId.commitmentOne, + _clockOne, + _leftNode, + _rightNode + ); + } else if (!_clockOne.hasTimeLeft() && _clockTwo.hasTimeLeft()) { + require( + _matchId.commitmentTwo.verify(_leftNode, _rightNode), + "child nodes do not match parent (commitmentTwo)" + ); + + _clockTwo.addValidatorEffort(_clockOne.timeSinceTimeout()); + pairCommitment( + _matchId.commitmentTwo, + _clockTwo, + _leftNode, + _rightNode + ); + } else { + revert("cannot win by timeout"); + } + + delete matches[_matchId.hashFromId()]; + } + + + // + // View methods + // + + function canWinMatchByTimeout( + Match.Id calldata _matchId + ) external view returns (bool) { + Clock.State memory _clockOne = clocks[_matchId.commitmentOne]; + Clock.State memory _clockTwo = clocks[_matchId.commitmentTwo]; + + return !_clockOne.hasTimeLeft() || !_clockTwo.hasTimeLeft(); + } + + function getCommitment( + Tree.Node _commitmentRoot + ) external view returns (Clock.State memory, Machine.Hash) { + return (clocks[_commitmentRoot], finalStates[_commitmentRoot]); + } + + function getMatch( + Match.IdHash _matchIdHash + ) public view returns (Match.State memory) { + return matches[_matchIdHash]; + } + + function getMatchCycle( + Match.IdHash _matchIdHash + ) external view returns (uint256) { + Match.State memory _m = getMatch(_matchIdHash); + return _m.toCycle(startCycle); + } + + function tournamentLevelConstants() + external + view + returns (uint64 _level, uint64 _log2step, uint64 _height) + { + _level = level; + _log2step = ArbitrationConstants.log2step(level); + _height = ArbitrationConstants.height(level); + } + + // + // Helper functions + // + + function requireValidContestedFinalState( + Machine.Hash _finalState + ) internal view { + require( + validContestedFinalState(_finalState), + "tournament doesn't have contested final state" + ); + } + + function hasDanglingCommitment() + internal + view + returns (bool _h, Tree.Node _node) + { + _node = danglingCommitment; + + if (!_node.isZero()) { + _h = true; + } + } + + function setDanglingCommitment(Tree.Node _node) internal { + danglingCommitment = _node; + } + + function clearDanglingCommitment() internal { + danglingCommitment = Tree.ZERO_NODE; + } + + function pairCommitment( + Tree.Node _rootHash, + Clock.State memory _newClock, + Tree.Node _leftNode, + Tree.Node _rightNode + ) internal { + assert(_leftNode.join(_rightNode).eq(_rootHash)); + ( + bool _hasDanglingCommitment, + Tree.Node _danglingCommitment + ) = hasDanglingCommitment(); + + if (_hasDanglingCommitment) { + (Match.IdHash _matchId, Match.State memory _matchState) = Match + .createMatch( + _danglingCommitment, + _rootHash, + _leftNode, + _rightNode, + level + ); + + matches[_matchId] = _matchState; + + Clock.State storage _firstClock = clocks[_danglingCommitment]; + Time.Instant _delay = Clock.deadline(_firstClock, _newClock); + Time.Duration _maxDuration = Clock.max(_firstClock, _newClock); + + setMaximumDelay(_delay); + updateParentTournamentDelay(_delay.add(_maxDuration)); // TODO hack + + _firstClock.advanceClock(); + + clearDanglingCommitment(); + updateParentTournamentDelay(_delay); + + emit matchCreated(_danglingCommitment, _rootHash, _leftNode); + } else { + updateParentTournamentDelay(maximumEnforceableDelay.add(_newClock.allowance)); + setDanglingCommitment(_rootHash); + } + } + + + // + // Clock methods + // + + /// @return bool if the tournament is still open to join + function isClosed() internal view returns (bool) { + if (allowance.gt(ArbitrationConstants.CENSORSHIP_TOLERANCE)) { + return + startInstant.timeoutElapsed( + ArbitrationConstants.CENSORSHIP_TOLERANCE + ); + } else { + return startInstant.timeoutElapsed(allowance); + } + } + + /// @return bool if the tournament is over + function isFinished() internal view returns (bool) { + return Time.currentTime().gt(maximumEnforceableDelay); + } + + function setMaximumDelay(Time.Instant _delay) internal returns (bool) { + if (_delay.gt(maximumEnforceableDelay)) { + maximumEnforceableDelay = _delay; + return true; + } else { + return false; + } + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/concretes/BottomTournament.sol b/onchain/permissionless-arbitration/src/tournament/concretes/BottomTournament.sol new file mode 100644 index 000000000..098e23c98 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/concretes/BottomTournament.sol @@ -0,0 +1,35 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../abstracts/LeafTournament.sol"; +import "../abstracts/NonRootTournament.sol"; + +/// @notice Bottom tournament of a multi-level instance +contract BottomTournament is LeafTournament, NonRootTournament { + constructor( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level, + NonLeafTournament _parent + ) + LeafTournament() + NonRootTournament( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level, + _parent + ) + {} +} diff --git a/onchain/permissionless-arbitration/src/tournament/concretes/MiddleTournament.sol b/onchain/permissionless-arbitration/src/tournament/concretes/MiddleTournament.sol new file mode 100644 index 000000000..c8861fff5 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/concretes/MiddleTournament.sol @@ -0,0 +1,38 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../abstracts/NonLeafTournament.sol"; +import "../abstracts/NonRootTournament.sol"; + +import "../factories/TournamentFactory.sol"; + +/// @notice Middle tournament is non-top, non-bottom of a multi-level instance +contract MiddleTournament is NonLeafTournament, NonRootTournament { + constructor( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level, + NonLeafTournament _parent, + TournamentFactory _tournamentFactory + ) + NonLeafTournament(_tournamentFactory) + NonRootTournament( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level, + _parent + ) + {} +} diff --git a/onchain/permissionless-arbitration/src/tournament/concretes/SingleLevelTournament.sol b/onchain/permissionless-arbitration/src/tournament/concretes/SingleLevelTournament.sol new file mode 100644 index 000000000..3a2a20dea --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/concretes/SingleLevelTournament.sol @@ -0,0 +1,16 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../abstracts/RootTournament.sol"; +import "../abstracts/LeafTournament.sol"; + +contract SingleLevelTournament is LeafTournament, RootTournament { + constructor( + Machine.Hash _initialHash + ) + LeafTournament() + RootTournament(_initialHash) + {} +} diff --git a/onchain/permissionless-arbitration/src/tournament/concretes/TopTournament.sol b/onchain/permissionless-arbitration/src/tournament/concretes/TopTournament.sol new file mode 100644 index 000000000..263bea69e --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/concretes/TopTournament.sol @@ -0,0 +1,22 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../abstracts/RootTournament.sol"; +import "../abstracts/NonLeafTournament.sol"; + +import "../factories/TournamentFactory.sol"; + +import "../../Machine.sol"; + +/// @notice Top tournament of a multi-level instance +contract TopTournament is NonLeafTournament, RootTournament { + constructor( + Machine.Hash _initialHash, + TournamentFactory _factory + ) + NonLeafTournament(_factory) + RootTournament(_initialHash) + {} +} diff --git a/onchain/permissionless-arbitration/src/tournament/factories/BottomTournamentFactory.sol b/onchain/permissionless-arbitration/src/tournament/factories/BottomTournamentFactory.sol new file mode 100644 index 000000000..c164907a3 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/factories/BottomTournamentFactory.sol @@ -0,0 +1,36 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../concretes/BottomTournament.sol"; + +contract BottomTournamentFactory { + constructor() {} + + function instantiate( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level, + NonLeafTournament _parent + ) external returns (BottomTournament) { + BottomTournament _tournament = new BottomTournament( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level, + _parent + ); + + return _tournament; + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/factories/ITournamentFactory.sol b/onchain/permissionless-arbitration/src/tournament/factories/ITournamentFactory.sol new file mode 100644 index 000000000..ad243d07a --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/factories/ITournamentFactory.sol @@ -0,0 +1,40 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../abstracts/Tournament.sol"; + +interface ITournamentFactory { + event rootCreated(Tournament); + + function instantiateSingleLevel( + Machine.Hash _initialHash + ) external returns (Tournament); + + function instantiateTop( + Machine.Hash _initialHash + ) external returns (Tournament); + + function instantiateMiddle( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level + ) external returns (Tournament); + + function instantiateBottom( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level + ) external returns (Tournament); +} diff --git a/onchain/permissionless-arbitration/src/tournament/factories/MiddleTournamentFactory.sol b/onchain/permissionless-arbitration/src/tournament/factories/MiddleTournamentFactory.sol new file mode 100644 index 000000000..409c0c2d7 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/factories/MiddleTournamentFactory.sol @@ -0,0 +1,43 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "./TournamentFactory.sol"; +import "../abstracts/NonLeafTournament.sol"; +import "../concretes/MiddleTournament.sol"; + +import "../../Machine.sol"; +import "../../Tree.sol"; +import "../../Time.sol"; + +contract MiddleTournamentFactory { + constructor() {} + + function instantiate( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level, + NonLeafTournament _parent + ) external returns (MiddleTournament) { + MiddleTournament _tournament = new MiddleTournament( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level, + _parent, + TournamentFactory(msg.sender) + ); + + return _tournament; + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/factories/SingleLevelTournamentFactory.sol b/onchain/permissionless-arbitration/src/tournament/factories/SingleLevelTournamentFactory.sol new file mode 100644 index 000000000..5ea95c4c5 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/factories/SingleLevelTournamentFactory.sol @@ -0,0 +1,20 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../concretes/SingleLevelTournament.sol"; + +contract SingleLevelTournamentFactory { + constructor() {} + + function instantiate( + Machine.Hash _initialHash + ) external returns (SingleLevelTournament) { + SingleLevelTournament _tournament = new SingleLevelTournament( + _initialHash + ); + + return _tournament; + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/factories/TopTournamentFactory.sol b/onchain/permissionless-arbitration/src/tournament/factories/TopTournamentFactory.sol new file mode 100644 index 000000000..2b4f5e04f --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/factories/TopTournamentFactory.sol @@ -0,0 +1,21 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../concretes/TopTournament.sol"; + +contract TopTournamentFactory { + constructor() {} + + function instantiate( + Machine.Hash _initialHash + ) external returns (TopTournament) { + TopTournament _tournament = new TopTournament( + _initialHash, + TournamentFactory(msg.sender) + ); + + return _tournament; + } +} diff --git a/onchain/permissionless-arbitration/src/tournament/factories/TournamentFactory.sol b/onchain/permissionless-arbitration/src/tournament/factories/TournamentFactory.sol new file mode 100644 index 000000000..fe3e61797 --- /dev/null +++ b/onchain/permissionless-arbitration/src/tournament/factories/TournamentFactory.sol @@ -0,0 +1,105 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import "../concretes/TopTournament.sol"; +import "../concretes/MiddleTournament.sol"; +import "../concretes/BottomTournament.sol"; +import "../concretes/SingleLevelTournament.sol"; + +import "./TopTournamentFactory.sol"; +import "./MiddleTournamentFactory.sol"; +import "./BottomTournamentFactory.sol"; +import "./SingleLevelTournamentFactory.sol"; + +contract TournamentFactory is ITournamentFactory { + SingleLevelTournamentFactory immutable singleLevelFactory; + TopTournamentFactory immutable topFactory; + MiddleTournamentFactory immutable middleFactory; + BottomTournamentFactory immutable bottomFactory; + + constructor( + SingleLevelTournamentFactory _singleLevelFactory, + TopTournamentFactory _topFactory, + MiddleTournamentFactory _middleFactory, + BottomTournamentFactory _bottomFactory + ) { + topFactory = _topFactory; + middleFactory = _middleFactory; + bottomFactory = _bottomFactory; + singleLevelFactory = _singleLevelFactory; + } + + function instantiateSingleLevel( + Machine.Hash _initialHash + ) external override returns (Tournament) { + SingleLevelTournament _tournament = singleLevelFactory.instantiate( + _initialHash + ); + emit rootCreated(_tournament); + + return _tournament; + } + + function instantiateTop( + Machine.Hash _initialHash + ) external override returns (Tournament) { + TopTournament _tournament = topFactory.instantiate( + _initialHash + ); + emit rootCreated(_tournament); + + return _tournament; + } + + function instantiateMiddle( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level + ) external override returns (Tournament) { + MiddleTournament _tournament = middleFactory.instantiate( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level, + NonLeafTournament(msg.sender) + ); + + return _tournament; + } + + function instantiateBottom( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level + ) external override returns (Tournament) { + BottomTournament _tournament = bottomFactory.instantiate( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level, + NonLeafTournament(msg.sender) + ); + + return _tournament; + } +} diff --git a/onchain/permissionless-arbitration/test/Match.t.sol b/onchain/permissionless-arbitration/test/Match.t.sol new file mode 100644 index 000000000..9cea84e76 --- /dev/null +++ b/onchain/permissionless-arbitration/test/Match.t.sol @@ -0,0 +1,168 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + +import "src/Match.sol"; +import "src/CanonicalConstants.sol"; + +pragma solidity ^0.8.0; + +// TODO: we cannot set the height of a match anymore +// To properly test, we'll need to swap the implementation of +// ArbitrationConstants. + +/* +contract MatchTest is Test { + using Tree for Tree.Node; + using Machine for Machine.Hash; + using Match for Match.Id; + using Match for Match.State; + + uint256 MAX_LOG2_SIZE = ArbitrationConstants.height(0); + + Match.State leftDivergenceMatch; + Match.State rightDivergenceMatch; + Match.IdHash leftDivergenceMatchId; + Match.IdHash rightDivergenceMatchId; + + function setUp() public { + Tree.Node leftDivergenceCommitment1 = Tree.ZERO_NODE.join( + Tree.ZERO_NODE + ); + Tree.Node rightDivergenceCommitment1 = Tree.ZERO_NODE.join( + Tree.ZERO_NODE + ); + + Tree.Node leftDivergenceCommitment2 = Tree + .Node + .wrap(bytes32(uint256(1))) + .join(Tree.ZERO_NODE); + Tree.Node rightDivergenceCommitment2 = Tree.ZERO_NODE.join( + Tree.Node.wrap(bytes32(uint256(1))) + ); + + (leftDivergenceMatchId, leftDivergenceMatch) = Match.createMatch( + leftDivergenceCommitment1, + leftDivergenceCommitment2, + Tree.Node.wrap(bytes32(uint256(1))), + Tree.ZERO_NODE, + 1 + ); + + (rightDivergenceMatchId, rightDivergenceMatch) = Match.createMatch( + rightDivergenceCommitment1, + rightDivergenceCommitment2, + Tree.ZERO_NODE, + Tree.Node.wrap(bytes32(uint256(1))), + 1 + ); + } + + function testDivergenceLeftWithEvenHeight() public { + assertTrue( + !leftDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), + "left node should diverge" + ); + ( + Machine.Hash _finalHashOne, + Machine.Hash _finalHashTwo + ) = leftDivergenceMatch.setDivergenceOnLeftLeaf(Tree.ZERO_NODE); + + leftDivergenceMatch.height = 2; + + assertTrue( + _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), + "hash one should be zero" + ); + assertTrue( + _finalHashTwo.eq( + Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() + ), + "hash two should be 1" + ); + } + + function testDivergenceRightWithEvenHeight() public { + assertTrue( + rightDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), + "left node should match" + ); + ( + Machine.Hash _finalHashOne, + Machine.Hash _finalHashTwo + ) = rightDivergenceMatch.setDivergenceOnRightLeaf(Tree.ZERO_NODE); + + rightDivergenceMatch.height = 2; + + assertTrue( + _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), + "hash one should be zero" + ); + assertTrue( + _finalHashTwo.eq( + Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() + ), + "hash two should be 1" + ); + } + + function testDivergenceLeftWithOddHeight() public { + assertTrue( + !leftDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), + "left node should diverge" + ); + ( + Machine.Hash _finalHashOne, + Machine.Hash _finalHashTwo + ) = leftDivergenceMatch.setDivergenceOnLeftLeaf(Tree.ZERO_NODE); + + leftDivergenceMatch.height = 3; + + assertTrue( + _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), + "hash one should be zero" + ); + assertTrue( + _finalHashTwo.eq( + Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() + ), + "hash two should be 1" + ); + } + + function testDivergenceRightWithOddHeight() public { + assertTrue( + rightDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), + "left node should match" + ); + ( + Machine.Hash _finalHashOne, + Machine.Hash _finalHashTwo + ) = rightDivergenceMatch.setDivergenceOnRightLeaf(Tree.ZERO_NODE); + + rightDivergenceMatch.height = 3; + + assertTrue( + _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), + "hash one should be zero" + ); + assertTrue( + _finalHashTwo.eq( + Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() + ), + "hash two should be 1" + ); + } +} +*/ diff --git a/onchain/permissionless-arbitration/test/MultiTournament.t.sol b/onchain/permissionless-arbitration/test/MultiTournament.t.sol new file mode 100644 index 000000000..4f5afeb3f --- /dev/null +++ b/onchain/permissionless-arbitration/test/MultiTournament.t.sol @@ -0,0 +1,384 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + +import "./Util.sol"; +import "src/tournament/factories/TournamentFactory.sol"; +import "src/CanonicalConstants.sol"; + +pragma solidity ^0.8.0; + +contract MultiTournamentTest is Test { + using Tree for Tree.Node; + using Time for Time.Instant; + using Match for Match.Id; + using Machine for Machine.Hash; + + // players' commitment node at different height + // player 0, player 1, and player 2 + Tree.Node[][3] playerNodes; + + TournamentFactory immutable factory; + TopTournament topTournament; + MiddleTournament middleTournament; + + event matchCreated( + Tree.Node indexed one, + Tree.Node indexed two, + Tree.Node leftOfTwo + ); + event newInnerTournament(Match.IdHash indexed, NonRootTournament); + + constructor() { + factory = Util.instantiateTournamentFactory(); + } + + function setUp() public { + playerNodes[0] = new Tree.Node[](ArbitrationConstants.height(0) + 1); + playerNodes[1] = new Tree.Node[](ArbitrationConstants.height(0) + 1); + playerNodes[2] = new Tree.Node[](ArbitrationConstants.height(0) + 1); + + playerNodes[0][0] = Tree.ZERO_NODE; + playerNodes[1][0] = Util.ONE_NODE; + playerNodes[2][0] = Util.ONE_NODE; + + for (uint256 _i = 1; _i <= ArbitrationConstants.height(0); _i++) { + // player 0 is all zero leaf node + playerNodes[0][_i] = playerNodes[0][_i - 1].join( + playerNodes[0][_i - 1] + ); + // player 1 is all 1 + playerNodes[1][_i] = playerNodes[1][_i - 1].join( + playerNodes[1][_i - 1] + ); + // player 2 is all 0 but right most leaf node is 1 + playerNodes[2][_i] = playerNodes[0][_i - 1].join( + playerNodes[2][_i - 1] + ); + } + } + + function testRootWinner() public { + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + + // no winner before tournament finished + (bool _finished, Tree.Node _winner, Machine.Hash _finalState) = topTournament + .arbitrationResult(); + + assertTrue(_winner.isZero(), "winner should be zero node"); + assertFalse(_finished, "tournament shouldn't be finished"); + assertTrue( + _finalState.eq(Machine.ZERO_STATE), + "final state should be zero" + ); + + // player 0 should win after fast forward time to tournament finishes + uint256 _t = block.timestamp; + uint256 _tournamentFinish = _t + + 1 + + Time.Duration.unwrap(ArbitrationConstants.CENSORSHIP_TOLERANCE); + + // the delay is doubled when a match is created + uint256 _tournamentFinishWithMatch = _tournamentFinish + + Time.Duration.unwrap(ArbitrationConstants.CENSORSHIP_TOLERANCE); + + vm.warp(_tournamentFinish); + (_finished, _winner, _finalState) = topTournament.arbitrationResult(); + + assertTrue( + _winner.eq(playerNodes[0][ArbitrationConstants.height(0)]), + "winner should be player 0" + ); + assertTrue(_finished, "tournament should be finished"); + assertTrue( + _finalState.eq(Tree.ZERO_NODE.toMachineHash()), + "final state should be zero" + ); + + // rewind time in half and pair commitment, expect a match + vm.warp(_t); + // player 1 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 1); + + // no dangling commitment available, should revert + vm.warp(_tournamentFinishWithMatch); + vm.expectRevert(); + topTournament.arbitrationResult(); + } + + function testInner() public { + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 1); + + Match.Id memory _matchId = Util.matchId(playerNodes, 1, 0); + + // advance match to end, this match will always advance to left tree + uint256 _playerToSeal = Util.advanceMatch01AtLevel( + playerNodes, + topTournament, + _matchId, + 0 + ); + + // seal match + topTournament.sealInnerMatchAndCreateInnerTournament( + _matchId, + playerNodes[_playerToSeal][0], + playerNodes[_playerToSeal][0], + Machine.ZERO_STATE, + Util.generateProof( + playerNodes, + _playerToSeal, + ArbitrationConstants.height(1) + ) + ); + + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + + // pair commitment, expect a match + // player 2 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 2); + + _matchId = Util.matchId(playerNodes, 2, 0); + + // advance match to end, this match will always advance to right tree + _playerToSeal = Util.advanceMatch02AtLevel( + playerNodes, + topTournament, + _matchId, + 0 + ); + + // seal match + topTournament.sealInnerMatchAndCreateInnerTournament( + _matchId, + playerNodes[0][0], + playerNodes[_playerToSeal][0], + Machine.ZERO_STATE, + Util.generateProof( + playerNodes, + _playerToSeal, + ArbitrationConstants.height(1) + ) + ); + } + + function testInnerWinner() public { + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 1); + + Match.Id memory _matchId = Util.matchId(playerNodes, 1, 0); + + // advance match to end, this match will always advance to left tree + uint256 _playerToSeal = Util.advanceMatch01AtLevel( + playerNodes, + topTournament, + _matchId, + 0 + ); + + // expect new inner created + vm.recordLogs(); + + // seal match + topTournament.sealInnerMatchAndCreateInnerTournament( + _matchId, + playerNodes[_playerToSeal][0], + playerNodes[_playerToSeal][0], + Machine.ZERO_STATE, + Util.generateProof( + playerNodes, + _playerToSeal, + ArbitrationConstants.height(1) + ) + ); + + Vm.Log[] memory _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], + Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + (bool _finished, Tree.Node _winner) = middleTournament.innerTournamentWinner(); + assertFalse(_finished, "winner should be zero node"); + + // player 0 should win after fast forward time to inner tournament finishes + uint256 _t = block.timestamp; + // the delay is doubled when a match is created + uint256 _rootTournamentFinish = _t + + 2 * + Time.Duration.unwrap(ArbitrationConstants.CENSORSHIP_TOLERANCE); + Util.joinMiddleTournament(playerNodes, middleTournament, 0, 1); + + vm.warp(_rootTournamentFinish - 1); + (_finished, _winner) = middleTournament.innerTournamentWinner(); + topTournament.winInnerMatch( + middleTournament, + playerNodes[0][ArbitrationConstants.height(0) - 1], + playerNodes[0][ArbitrationConstants.height(0) - 1] + ); + + { + vm.warp(_rootTournamentFinish + 1); + (bool _finishedTop, Tree.Node _commitment, Machine.Hash _finalState) = topTournament + .arbitrationResult(); + + assertTrue( + _commitment.eq(playerNodes[0][ArbitrationConstants.height(0)]), + "winner should be player 0" + ); + assertTrue(_finishedTop, "tournament should be finished"); + assertTrue( + _finalState.eq(Tree.ZERO_NODE.toMachineHash()), + "final state should be zero" + ); + } + + //create another tournament for other test + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 1); + + _matchId = Util.matchId(playerNodes, 1, 0); + + // advance match to end, this match will always advance to left tree + _playerToSeal = Util.advanceMatch01AtLevel( + playerNodes, + topTournament, + _matchId, + 0 + ); + + // expect new inner created + vm.recordLogs(); + + // seal match + topTournament.sealInnerMatchAndCreateInnerTournament( + _matchId, + playerNodes[_playerToSeal][0], + playerNodes[_playerToSeal][0], + Machine.ZERO_STATE, + Util.generateProof( + playerNodes, + _playerToSeal, + ArbitrationConstants.height(1) + ) + ); + + _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], + Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + (_finished, _winner) = middleTournament.innerTournamentWinner(); + assertTrue(_winner.isZero(), "winner should be zero node"); + + _t = block.timestamp; + // the delay is doubled when a match is created + uint256 _middleTournamentFinish = _t + + 1 + + 2 * + Time.Duration.unwrap(ArbitrationConstants.CENSORSHIP_TOLERANCE); + _rootTournamentFinish = + _middleTournamentFinish + + 2 * + Time.Duration.unwrap(ArbitrationConstants.CENSORSHIP_TOLERANCE); + + Util.joinMiddleTournament(playerNodes, middleTournament, 0, 1); + + //let player 1 join, then timeout player 0 + Util.joinMiddleTournament(playerNodes, middleTournament, 1, 1); + + (Clock.State memory _player0Clock, ) = middleTournament.getCommitment( + playerNodes[0][ArbitrationConstants.height(1)] + ); + vm.warp( + Time.Instant.unwrap( + _player0Clock.startInstant.add(_player0Clock.allowance) + ) + ); + _matchId = Util.matchId(playerNodes, 1, 1); + middleTournament.winMatchByTimeout( + _matchId, + playerNodes[1][ArbitrationConstants.height(1) - 1], + playerNodes[1][ArbitrationConstants.height(1) - 1] + ); + + vm.warp(_middleTournamentFinish); + (_finished, _winner) = middleTournament.innerTournamentWinner(); + topTournament.winInnerMatch( + middleTournament, + playerNodes[1][ArbitrationConstants.height(0) - 1], + playerNodes[1][ArbitrationConstants.height(0) - 1] + ); + + { + vm.warp(_rootTournamentFinish); + (bool _finishedTop, Tree.Node _commitment, Machine.Hash _finalState) = topTournament + .arbitrationResult(); + + assertTrue( + _commitment.eq(playerNodes[1][ArbitrationConstants.height(0)]), + "winner should be player 1" + ); + assertTrue(_finishedTop, "tournament should be finished"); + assertTrue( + _finalState.eq(Util.ONE_NODE.toMachineHash()), + "final state should be 1" + ); + } + } +} diff --git a/onchain/permissionless-arbitration/test/Tournament.t.sol b/onchain/permissionless-arbitration/test/Tournament.t.sol new file mode 100644 index 000000000..2bf3d5656 --- /dev/null +++ b/onchain/permissionless-arbitration/test/Tournament.t.sol @@ -0,0 +1,205 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + +import "./Util.sol"; +import "src/tournament/factories/TournamentFactory.sol"; +import "src/CanonicalConstants.sol"; + +pragma solidity ^0.8.0; + +contract TournamentTest is Test { + using Tree for Tree.Node; + using Time for Time.Instant; + using Match for Match.Id; + using Machine for Machine.Hash; + + // players' commitment node at different height + // player 0, player 1, and player 2 + Tree.Node[][3] playerNodes; + Tree.Node constant ONE_NODE = Tree.Node.wrap(bytes32(uint256(1))); + + TournamentFactory immutable factory; + TopTournament topTournament; + MiddleTournament middleTournament; + + event matchCreated( + Tree.Node indexed one, + Tree.Node indexed two, + Tree.Node leftOfTwo + ); + event newInnerTournament(Match.IdHash indexed, NonRootTournament); + + constructor() { + factory = Util.instantiateTournamentFactory(); + } + + function setUp() public { + playerNodes[0] = new Tree.Node[](ArbitrationConstants.height(0) + 1); + playerNodes[1] = new Tree.Node[](ArbitrationConstants.height(0) + 1); + playerNodes[2] = new Tree.Node[](ArbitrationConstants.height(0) + 1); + + playerNodes[0][0] = Tree.ZERO_NODE; + playerNodes[1][0] = ONE_NODE; + playerNodes[2][0] = ONE_NODE; + + for (uint256 _i = 1; _i <= ArbitrationConstants.height(0); _i++) { + // player 0 is all zero leaf node + playerNodes[0][_i] = playerNodes[0][_i - 1].join( + playerNodes[0][_i - 1] + ); + // player 1 is all 1 + playerNodes[1][_i] = playerNodes[1][_i - 1].join( + playerNodes[1][_i - 1] + ); + // player 2 is all 0 but right most leaf node is 1 + playerNodes[2][_i] = playerNodes[0][_i - 1].join( + playerNodes[2][_i - 1] + ); + } + } + + function testJoinTournament() public { + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + + // duplicate commitment should be reverted + vm.expectRevert("clock is initialized"); + Util.joinTopTournament(playerNodes, topTournament, 0); + + // pair commitment, expect a match + vm.expectEmit(true, true, false, true, address(topTournament)); + emit matchCreated( + playerNodes[0][ArbitrationConstants.height(0) - 0], + playerNodes[1][ArbitrationConstants.height(0) - 0], + playerNodes[1][ArbitrationConstants.height(0) - 1] + ); + // player 1 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 1); + } + + function testTimeout() public { + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + + uint256 _t = block.timestamp; + // the delay is doubled when a match is created + uint256 _tournamentFinishWithMatch = _t + + 1 + + 2 * + Time.Duration.unwrap(ArbitrationConstants.CENSORSHIP_TOLERANCE); + + // player 1 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 1); + + Match.Id memory _matchId = Util.matchId(playerNodes, 1, 0); + assertFalse( + topTournament.canWinMatchByTimeout(_matchId), + "shouldn't be able to win match by timeout" + ); + + // player 1 should win after fast forward time to player 0 timeout + // player 0 timeout first because he's supposed to advance match first after the match is created + (Clock.State memory _player0Clock, ) = topTournament.getCommitment( + playerNodes[0][ArbitrationConstants.height(0)] + ); + vm.warp( + Time.Instant.unwrap( + _player0Clock.startInstant.add(_player0Clock.allowance) + ) + ); + assertTrue( + topTournament.canWinMatchByTimeout(_matchId), + "should be able to win match by timeout" + ); + topTournament.winMatchByTimeout( + _matchId, + playerNodes[1][ArbitrationConstants.height(0) - 1], + playerNodes[1][ArbitrationConstants.height(0) - 1] + ); + + vm.warp(_tournamentFinishWithMatch); + (bool _finished, Tree.Node _winner, Machine.Hash _finalState) = topTournament + .arbitrationResult(); + + assertTrue( + _winner.eq(playerNodes[1][ArbitrationConstants.height(0)]), + "winner should be player 1" + ); + assertTrue(_finished, "tournament should be finished"); + assertTrue(_finalState.eq(Util.ONE_STATE), "final state should be 1"); + + topTournament = Util.initializePlayer0Tournament( + playerNodes, + factory + ); + _t = block.timestamp; + + // the delay is doubled when a match is created + _tournamentFinishWithMatch = + _t + + 1 + + 2 * + Time.Duration.unwrap(ArbitrationConstants.CENSORSHIP_TOLERANCE); + + // player 1 joins tournament + Util.joinTopTournament(playerNodes, topTournament, 1); + + // player 0 should win after fast forward time to player 1 timeout + // player 1 timeout first because he's supposed to advance match after player 0 advanced + _matchId = Util.matchId(playerNodes, 1, 0); + + topTournament.advanceMatch( + _matchId, + playerNodes[0][ArbitrationConstants.height(0) - 1], + playerNodes[0][ArbitrationConstants.height(0) - 1], + playerNodes[0][ArbitrationConstants.height(0) - 2], + playerNodes[0][ArbitrationConstants.height(0) - 2] + ); + (Clock.State memory _player1Clock, ) = topTournament.getCommitment( + playerNodes[1][ArbitrationConstants.height(0)] + ); + vm.warp( + Time.Instant.unwrap( + _player1Clock.startInstant.add(_player1Clock.allowance) + ) + ); + assertTrue( + topTournament.canWinMatchByTimeout(_matchId), + "should be able to win match by timeout" + ); + topTournament.winMatchByTimeout( + _matchId, + playerNodes[0][ArbitrationConstants.height(0) - 1], + playerNodes[0][ArbitrationConstants.height(0) - 1] + ); + + vm.warp(_tournamentFinishWithMatch); + (_finished, _winner, _finalState) = topTournament.arbitrationResult(); + + assertTrue( + _winner.eq(playerNodes[0][ArbitrationConstants.height(0)]), + "winner should be player 0" + ); + assertTrue(_finished, "tournament should be finished"); + assertTrue( + _finalState.eq(Tree.ZERO_NODE.toMachineHash()), + "final state should be zero" + ); + } +} diff --git a/onchain/permissionless-arbitration/test/TournamentFactory.t.sol b/onchain/permissionless-arbitration/test/TournamentFactory.t.sol new file mode 100644 index 000000000..e2a0a3b26 --- /dev/null +++ b/onchain/permissionless-arbitration/test/TournamentFactory.t.sol @@ -0,0 +1,70 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + +import "src/tournament/abstracts/RootTournament.sol"; +import "src/tournament/factories/TournamentFactory.sol"; +import "src/CanonicalConstants.sol"; + +import "./Util.sol"; + +pragma solidity ^0.8.0; + +contract TournamentFactoryTest is Test { + TournamentFactory factory; + + function setUp() public { + factory = Util.instantiateTournamentFactory(); + } + + function testRootTournament() public { + RootTournament rootTournament = RootTournament(address(factory.instantiateSingleLevel( + Machine.ZERO_STATE + ))); + + (uint64 _level, uint64 _log2step, uint64 _height) = rootTournament + .tournamentLevelConstants(); + + assertEq(_level, 0, "level should be 0"); + assertEq( + _log2step, + ArbitrationConstants.log2step(_level), + "log2step should match" + ); + assertEq( + _height, + ArbitrationConstants.height(_level), + "height should match" + ); + + rootTournament = RootTournament(address(factory.instantiateTop( + Machine.ZERO_STATE + ))); + + (_level, _log2step, _height) = rootTournament + .tournamentLevelConstants(); + + assertEq(_level, 0, "level should be 0"); + assertEq( + _log2step, + ArbitrationConstants.log2step(_level), + "log2step should match" + ); + assertEq( + _height, + ArbitrationConstants.height(_level), + "height should match" + ); + } +} diff --git a/onchain/permissionless-arbitration/test/Util.sol b/onchain/permissionless-arbitration/test/Util.sol new file mode 100644 index 000000000..2f887133a --- /dev/null +++ b/onchain/permissionless-arbitration/test/Util.sol @@ -0,0 +1,233 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "src/Match.sol"; +import "src/CanonicalConstants.sol"; +import "src/tournament/concretes/TopTournament.sol"; +import "src/tournament/concretes/MiddleTournament.sol"; + +import "src/tournament/factories/SingleLevelTournamentFactory.sol"; +import "src/tournament/factories/TopTournamentFactory.sol"; +import "src/tournament/factories/MiddleTournamentFactory.sol"; +import "src/tournament/factories/BottomTournamentFactory.sol"; + +pragma solidity ^0.8.0; + +library Util { + using Tree for Tree.Node; + using Machine for Machine.Hash; + using Match for Match.Id; + using Match for Match.State; + + Tree.Node constant ONE_NODE = Tree.Node.wrap(bytes32(uint256(1))); + Machine.Hash constant ONE_STATE = Machine.Hash.wrap(bytes32(uint256(1))); + Machine.Hash constant TWO_STATE = Machine.Hash.wrap(bytes32(uint256(2))); + + function generateProof( + Tree.Node[][3] memory _playerNodes, + uint256 _player, + uint64 _height + ) internal pure returns (bytes32[] memory) { + bytes32[] memory _proof = new bytes32[](_height); + for (uint64 _i = 0; _i < _height; _i++) { + _proof[_i] = Tree.Node.unwrap(_playerNodes[_player][_i]); + } + return _proof; + } + + // advance match between player 0 and player 1 + function advanceMatch01AtLevel( + Tree.Node[][3] memory _playerNodes, + TopTournament _topTournament, + Match.Id memory _matchId, + uint64 _level + ) internal returns (uint256 _playerToSeal) { + uint256 _current = ArbitrationConstants.height(_level); + for (_current; _current > 1; _current -= 1) { + if (_playerToSeal == 0) { + // advance match alternately until it can be sealed + // starts with player 0 + _topTournament.advanceMatch( + _matchId, + _playerNodes[0][_current - 1], + _playerNodes[0][_current - 1], + _playerNodes[0][_current - 2], + _playerNodes[0][_current - 2] + ); + _playerToSeal = 1; + } else { + _topTournament.advanceMatch( + _matchId, + _playerNodes[1][_current - 1], + _playerNodes[1][_current - 1], + _playerNodes[1][_current - 2], + _playerNodes[1][_current - 2] + ); + _playerToSeal = 0; + } + } + } + + // advance match between player 0 and player 2 + function advanceMatch02AtLevel( + Tree.Node[][3] memory _playerNodes, + TopTournament _topTournament, + Match.Id memory _matchId, + uint64 _level + ) internal returns (uint256 _playerToSeal) { + uint256 _current = ArbitrationConstants.height(_level); + for (_current; _current > 1; _current -= 1) { + if (_playerToSeal == 0) { + // advance match alternately until it can be sealed + // starts with player 0 + _topTournament.advanceMatch( + _matchId, + _playerNodes[0][_current - 1], + _playerNodes[0][_current - 1], + _playerNodes[0][_current - 2], + _playerNodes[0][_current - 2] + ); + _playerToSeal = 2; + } else { + _topTournament.advanceMatch( + _matchId, + _playerNodes[0][_current - 1], + _playerNodes[2][_current - 1], + _playerNodes[0][_current - 2], + _playerNodes[2][_current - 2] + ); + _playerToSeal = 0; + } + } + } + + // create new _topTournament and player 0 joins it + function initializePlayer0Tournament( + Tree.Node[][3] memory _playerNodes, + TournamentFactory _factory + ) internal returns (TopTournament _topTournament) { + _topTournament = TopTournament( + address(_factory.instantiateTop(Machine.ZERO_STATE)) + ); + // player 0 joins tournament + joinTopTournament(_playerNodes, _topTournament, 0); + } + + // _player joins _topTournament + function joinTopTournament( + Tree.Node[][3] memory _playerNodes, + TopTournament _topTournament, + uint256 _player + ) internal { + if (_player == 0) { + _topTournament.joinTournament( + Machine.ZERO_STATE, + generateProof( + _playerNodes, + _player, + ArbitrationConstants.height(0) + ), + _playerNodes[0][ArbitrationConstants.height(0) - 1], + _playerNodes[0][ArbitrationConstants.height(0) - 1] + ); + } else if (_player == 1) { + _topTournament.joinTournament( + ONE_STATE, + generateProof( + _playerNodes, + _player, + ArbitrationConstants.height(0) + ), + _playerNodes[1][ArbitrationConstants.height(0) - 1], + _playerNodes[1][ArbitrationConstants.height(0) - 1] + ); + } else if (_player == 2) { + _topTournament.joinTournament( + TWO_STATE, + generateProof( + _playerNodes, + _player, + ArbitrationConstants.height(0) + ), + _playerNodes[0][ArbitrationConstants.height(0) - 1], + _playerNodes[2][ArbitrationConstants.height(0) - 1] + ); + } + } + + // _player joins _middleTournament at _level + function joinMiddleTournament( + Tree.Node[][3] memory _playerNodes, + MiddleTournament _middleTournament, + uint256 _player, + uint64 _level + ) internal { + if (_player == 0) { + _middleTournament.joinTournament( + Machine.ZERO_STATE, + generateProof( + _playerNodes, + _player, + ArbitrationConstants.height(_level) + ), + _playerNodes[0][ArbitrationConstants.height(_level) - 1], + _playerNodes[0][ArbitrationConstants.height(_level) - 1] + ); + } else if (_player == 1) { + _middleTournament.joinTournament( + ONE_STATE, + generateProof( + _playerNodes, + _player, + ArbitrationConstants.height(_level) + ), + _playerNodes[1][ArbitrationConstants.height(_level) - 1], + _playerNodes[1][ArbitrationConstants.height(_level) - 1] + ); + } else if (_player == 2) { + _middleTournament.joinTournament( + TWO_STATE, + generateProof( + _playerNodes, + _player, + ArbitrationConstants.height(_level) + ), + _playerNodes[0][ArbitrationConstants.height(_level) - 1], + _playerNodes[2][ArbitrationConstants.height(_level) - 1] + ); + } + } + + // create match id for player 0 and _opponent at _level + function matchId( + Tree.Node[][3] memory _playerNodes, + uint256 _opponent, + uint64 _level + ) internal pure returns (Match.Id memory) { + return + Match.Id( + _playerNodes[0][ArbitrationConstants.height(_level)], + _playerNodes[_opponent][ArbitrationConstants.height(_level)] + ); + } + + + // instantiates all sub-factories and TournamentFactory + function instantiateTournamentFactory() internal returns (TournamentFactory) { + SingleLevelTournamentFactory singleLevelFactory = new SingleLevelTournamentFactory(); + TopTournamentFactory topFactory = new TopTournamentFactory(); + MiddleTournamentFactory middleFactory = new MiddleTournamentFactory(); + BottomTournamentFactory bottomFactory = new BottomTournamentFactory(); + + return new TournamentFactory(singleLevelFactory, topFactory, middleFactory, bottomFactory); + } +}