diff --git a/.github/workflows/master-push.yml b/.github/workflows/master-push.yml deleted file mode 100644 index 8fd35489f..000000000 --- a/.github/workflows/master-push.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: 'Master Push' -on: - push: - branches: - - master - -jobs: - - release: - name: 'Publish Release' - runs-on: ubuntu-latest - environment: production - steps: - - name: 'Check out code' - uses: actions/checkout@v3 - with: - ref: ${{ github.event.push.head.sha }} - fetch-depth: 0 - - name: 'Make release' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -x - VERSION=v$(cat package/version) - gh release create ${VERSION} --target ${{ github.sha }} - - name: 'Update dependents' - run: | - set -x - version="$(cat package/version)" - curl --fail \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.JENKINS_GITHUB_PAT }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/runtimeverification/devops/dispatches \ - -d '{"event_type":"on-demand-test","client_payload":{"repo":"runtimeverification/wasm-semantics","version":"'${version}'"}}' - - nix-cache: - name: 'Populate Nix Cache' - strategy: - matrix: - include: - - runner: normal - - runner: ARM64 - runs-on: ${{ matrix.runner }} - steps: - - name: 'Check out code' - uses: actions/checkout@v3 - with: - ref: ${{ github.event.push.head.sha }} - fetch-depth: 0 - - name: 'Upgrade bash' - if: ${{ contains(matrix.os, 'macos') }} - run: brew install bash - - name: 'Build and cache KWASM' - uses: workflow/nix-shell-action@v3.0.3 - env: - GC_DONT_GC: 1 - CACHIX_AUTH_TOKEN: '${{ secrets.CACHIX_PUBLIC_TOKEN }}' - with: - packages: jq - script: | - kwasm=$(nix build --extra-experimental-features 'nix-command flakes' .#kwasm --json | jq -r '.[].outputs | to_entries[].value') - drv=$(nix-store --query --deriver ${kwasm}) - nix-store --query --requisites --include-outputs ${drv} | cachix push k-framework diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index 281a80a6a..240169a75 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -1,6 +1,9 @@ name: 'Run Tests' on: pull_request: + push: + branches: + - 'master-update' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -8,41 +11,14 @@ concurrency: jobs: - version-bump: - name: 'Version Bump' - runs-on: [self-hosted, linux, flyweight-ephemeral] - steps: - - name: 'Check out code' - uses: actions/checkout@v3 - with: - token: ${{ secrets.JENKINS_GITHUB_PAT }} - # fetch-depth 0 means deep clone the repo - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - name: 'Configure GitHub user' - run: | - git config user.name devops - git config user.email devops@runtimeverification.com - - name: 'Update version' - run: | - og_version=$(git show origin/${GITHUB_BASE_REF}:package/version) - ./package/version.sh bump ${og_version} - ./package/version.sh sub - new_version=$(cat package/version) - git add --update && git commit --message "Set Version: ${new_version}" || true - - name: 'Push updates' - run: git push origin HEAD:${GITHUB_HEAD_REF} - pykwasm-code-quality-checks: name: 'Code Quality Checks' runs-on: ubuntu-latest steps: - name: 'Check out code' - uses: actions/checkout@v3 - with: - submodules: recursive + uses: actions/checkout@v4 - name: 'Install Poetry' - uses: Gr1N/setup-poetry@v8 + uses: Gr1N/setup-poetry@v9 - name: 'Build pykwasm' run: poetry -C pykwasm install - name: 'Run code quality checks' @@ -51,11 +27,13 @@ jobs: run: make -C pykwasm pyupgrade - name: 'Run unit tests' run: make -C pykwasm cov-unit + - name: 'Run Rust unit-tests' + run: make rust-tests conformance-tests: name: 'Conformance Tests' needs: pykwasm-code-quality-checks - runs-on: [self-hosted, linux, normal] + runs-on: [self-hosted, aws, us-east-2, m6a.4xlarge] timeout-minutes: 30 steps: - name: 'Check out code' @@ -82,52 +60,3 @@ jobs: if: always() run: | docker stop --time=0 kwasm-ci-conformance-${GITHUB_SHA} - - prove-tests: - name: 'Prover Tests' - needs: pykwasm-code-quality-checks - runs-on: [self-hosted, linux, normal] - timeout-minutes: 30 - steps: - - name: 'Check out code' - uses: actions/checkout@v3 - with: - submodules: recursive - - name: 'Set up Docker' - uses: ./.github/actions/with-docker - with: - container-name: kwasm-ci-prove-${{ github.sha }} - - name: 'Build pykwasm' - run: docker exec -u user kwasm-ci-prove-${GITHUB_SHA} poetry -C pykwasm install - - name: 'Build Haskell definitions' - run: docker exec -u user kwasm-ci-prove-${GITHUB_SHA} poetry -C pykwasm run kdist -v build wasm-semantics.{kwasm-lemmas,wrc20} -j2 - - name: 'Run prover tests' - run: docker exec -u user kwasm-ci-prove-${GITHUB_SHA} make -j6 test-prove - - name: 'Tear down Docker' - if: always() - run: | - docker stop --time=0 kwasm-ci-prove-${GITHUB_SHA} - - nix: - name: 'Nix' - strategy: - fail-fast: false - matrix: - include: - - runner: normal - - runner: ARM64 - needs: pykwasm-code-quality-checks - runs-on: ${{ matrix.runner }} - timeout-minutes: 60 - steps: - - name: 'Check out code' - uses: actions/checkout@v3 - with: - # Check out pull request HEAD instead of merge commit. - ref: ${{ github.event.pull_request.head.sha }} - - name: 'Build KWASM' - run: GC_DONT_GC=1 nix build .#kwasm --extra-experimental-features 'nix-command flakes' --print-build-logs - - name: 'Build KWASM-Pyk' - run: GC_DONT_GC=1 nix build .#kwasm-pyk --extra-experimental-features 'nix-command flakes' --print-build-logs - - name: 'Test KWASM' - run: GC_DONT_GC=1 nix build .#kwasm-test --extra-experimental-features 'nix-command flakes' --print-build-logs diff --git a/.github/workflows/update-version.yml b/.github/workflows/update-version.yml index 566f84162..b1daeeec1 100644 --- a/.github/workflows/update-version.yml +++ b/.github/workflows/update-version.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: 'Check out code' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive token: ${{ secrets.JENKINS_GITHUB_PAT }} @@ -24,30 +24,11 @@ jobs: git config user.email devops@runtimeverification.com - name: 'Install Poetry' uses: Gr1N/setup-poetry@v8 - - name: 'Install Nix' - uses: cachix/install-nix-action@v22 - with: - install_url: https://releases.nixos.org/nix/nix-2.13.3/install - extra_nix_config: | - access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - substituters = http://cache.nixos.org https://hydra.iohk.io - trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= - - name: 'Install Cachix' - uses: cachix/cachix-action@v14 - with: - name: k-framework - authToken: '${{ secrets.CACHIX_PUBLIC_TOKEN }}' - name: 'Update pyk release tag' run: | K_VERSION=$(cat deps/k_release) sed -i 's!kframework = "[v0-9\.]*"!kframework = "'${K_VERSION}'"!' pykwasm/pyproject.toml poetry -C pykwasm update git add pykwasm/ && git commit -m "pykwasm/: sync poetry files ${K_VERSION}" || true - - name: 'Update Nix flake inputs' - run: | - K_VERSION=v"$(cat deps/k_release)" - sed -i 's! k-framework.url = "github:runtimeverification/k/v[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+"! k-framework.url = "github:runtimeverification/k/'"${K_VERSION}"'"!' flake.nix - nix flake update - git add flake.nix flake.lock && git commit -m 'flake.{nix,lock}: update Nix derivations' || true - name: 'Push updates' run: git push diff --git a/.gitignore b/.gitignore index 90b2bde72..02d531c72 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ .envrc result + +build diff --git a/Makefile b/Makefile index 3a5ad4abf..a02682571 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,190 @@ clean: pykwasm $(KDIST) clean +# Building ULM-integrated Definition +# ---------------------------------- + +ULM_BUILD_DIR=./build +ULM_LIB_DIR=$(ULM_BUILD_DIR)/lib +ULM_DEP_DIR=$(ULM_BUILD_DIR)/deps +ULM_CXX=$(shell [ $(origin CXX) != default ] && echo $(CXX) ) +ULM_KF_INCLUDE_DIR=$(shell dirname "`which llvm-kompile`")/../include + +ULM_WASM_DIR=$(ULM_BUILD_DIR)/wasm +ULM_WASM_SRC_DIR=pykwasm/src/pykwasm/kdist/wasm-semantics +ULM_WASM_LIB=libkwasm.so +ULM_WASM_BIN=interpreter +ULM_WASM_TARGET=$(ULM_LIB_DIR)/$(ULM_WASM_LIB) +ULM_WASM_MAIN=$(ULM_WASM_SRC_DIR)/ulm-wasm.md +ULM_WASM_SRC=$(wildcard $(ULM_WASM_SRC_DIR)/*.md $(ULM_WASM_SRC_DIR)/data/*.k) + +ULM_WASM_COMPILER_TARGET=$(ULM_BUILD_DIR)/ulm-contract-compiler + +## Depedencies + +ULM_KRYPTO_DIR=$(ULM_DEP_DIR)/plugin +ULM_KRYPTO_LIB=krypto.a +ULM_KRYPTO_TARGET=$(ULM_LIB_DIR)/$(ULM_KRYPTO_LIB) + +ULM_KEVM_DIR=$(ULM_DEP_DIR)/evm-semantics +ULM_KEVM_BUILD_DIR=$(ULM_KEVM_DIR)/libkevm +ULM_KEVM_BRANCH=ulm +ULM_KEVM_LIB=libkevm.so +ULM_KEVM_TARGET=$(ULM_LIB_DIR)/$(ULM_KEVM_LIB) + +ULM_CLONE_DIR=$(ULM_DEP_DIR)/ulm +ULM_HOOKS_DIR=$(ULM_CLONE_DIR)/kllvm +ULM_HOOKS_SRC=ulm_kllvm.cpp ulm_hooks.cpp ulm_kllvm_c.cpp +ULM_HOOKS_LIB=libulmkllvm.so +ULM_HOOKS_TARGET=$(ULM_LIB_DIR)/$(ULM_HOOKS_LIB) + +ULM_GETH_TARGET=$(ULM_BUILD_DIR)/geth + +### ULM Crypto Plugin + +$(ULM_KRYPTO_DIR)/.git: + @mkdir -p $(ULM_DEP_DIR) + cd $(ULM_DEP_DIR); \ + git clone --depth 1 https://github.com/runtimeverification/blockchain-k-plugin plugin; \ + cd plugin; \ + git submodule update --init --recursive + +$(ULM_KRYPTO_TARGET): | $(ULM_KRYPTO_DIR)/.git + @mkdir -p $(ULM_LIB_DIR) + $(if $(ULM_CXX), CXX=$(ULM_CXX)) make -C "$(ULM_KRYPTO_DIR)" build + cp "$(ULM_KRYPTO_DIR)/build/krypto/lib/krypto.a" "$(ULM_LIB_DIR)" + +.PHONY: ulm-krypto-build +ulm-krypto-build: $(ULM_KRYPTO_TARGET) + +### ULM Hooks + +$(ULM_CLONE_DIR)/.git: + @mkdir -p $(ULM_DEP_DIR) + cd $(ULM_DEP_DIR); \ + git clone --depth 1 https://github.com/pi-squared-inc/ulm + +$(ULM_HOOKS_TARGET): | $(ULM_CLONE_DIR)/.git + @mkdir -p $(ULM_LIB_DIR) + cd $(ULM_HOOKS_DIR); \ + $(CXX) -shared -o "$(ULM_HOOKS_LIB)" $(ULM_HOOKS_SRC) -I "$(ULM_KF_INCLUDE_DIR)" -I "$(ULM_KF_INCLUDE_DIR)/kllvm" \ + -fPIC -lcryptopp -lgmp -std=c++20 -Wall -Werror -g -fno-omit-frame-pointer -Wno-return-type-c-linkage $(CPPFLAGS) + cp "$(ULM_HOOKS_DIR)/$(ULM_HOOKS_LIB)" "$(ULM_LIB_DIR)" + +.PHONY: ulm-hooks-build +ulm-hooks-build: $(ULM_HOOKS_TARGET) + +### KEVM + +$(ULM_KEVM_DIR)/.git: + @mkdir -p $(ULM_DEP_DIR) + cd $(ULM_DEP_DIR); \ + git clone --depth 1 https://github.com/pi-squared-inc/evm-semantics -b $(ULM_KEVM_BRANCH) evm-semantics + +$(ULM_KEVM_TARGET): $(ULM_KRYPTO_TARGET) $(ULM_HOOKS_TARGET) | $(ULM_KEVM_DIR)/.git + @mkdir -p $(ULM_LIB_DIR) + kompile "$(ULM_KEVM_DIR)/kevm-pyk/src/kevm_pyk/kproj/evm-semantics/evm.md" \ + --main-module EVM \ + --syntax-module EVM \ + -I "$(ULM_KEVM_DIR)/kevm-pyk/src/kevm_pyk/kproj/evm-semantics" \ + -I "$(ULM_KRYPTO_DIR)" \ + -I "$(ULM_HOOKS_DIR)" \ + --md-selector 'k & ! symbolic' \ + --hook-namespaces 'JSON KRYPTO ULM' \ + --backend llvm \ + -O3 \ + -ccopt -std=c++20 \ + -ccopt -lssl \ + -ccopt -lcrypto \ + -ccopt -lsecp256k1 \ + -ccopt "$(ULM_LIB_DIR)/krypto.a" \ + -ccopt -Wno-deprecated-declarations \ + --output-definition "$(ULM_KEVM_BUILD_DIR)" \ + --type-inference-mode simplesub \ + --verbose \ + -ccopt -L"$(ULM_LIB_DIR)" \ + -ccopt -lulmkllvm \ + --llvm-kompile-type library \ + --llvm-kompile-output libkevm.so \ + -ccopt -g \ + --llvm-mutable-bytes \ + -ccopt "$(ULM_HOOKS_DIR)/lang/ulm_language_entry.cpp" \ + -ccopt -I"$(ULM_HOOKS_DIR)" \ + -ccopt -DULM_LANG_ID=kevm \ + --llvm-hidden-visibility \ + -ccopt -fPIC \ + -ccopt -shared + cp "$(ULM_KEVM_BUILD_DIR)"/libkevm.so "$(ULM_LIB_DIR)" + +.PHONY: kevm-build +kevm-build: $(ULM_KEVM_TARGET) + +### ULM + +$(ULM_GETH_TARGET): $(ULM_KEVM_TARGET) | $(ULM_CLONE_DIR) + cd $(ULM_CLONE_DIR)/op-geth && make + cp $(ULM_CLONE_DIR)/op-geth/build/bin/geth $(ULM_BUILD_DIR) + +.PHONY: ulm-build +ulm-build: $(ULM_GETH_TARGET) + +### ULM Wasm + +ULM_WASM_TYPE = $(if $(ULM_TEST),main,library) +ULM_WASM_OUT = $(if $(ULM_TEST),$(ULM_WASM_BIN),$(ULM_WASM_LIB)) +ULM_WASM_SEL = $(if $(ULM_TEST),k|local,k|remote) +ULM_LIB_FLAGS = $(if $(ULM_TEST),,-ccopt -L"$(ULM_LIB_DIR)" -ccopt -lulmkllvm -ccopt -shared -ccopt -fPIC -ccopt "$(ULM_HOOKS_DIR)/lang/ulm_language_entry.cpp") +ULM_HOOK_NMSP = $(if $(ULM_TEST),,ULM) + +$(ULM_WASM_TARGET): $(ULM_KRYPTO_TARGET) $(ULM_HOOKS_TARGET) $(ULM_WASM_SRC) + kompile \ + --hook-namespaces 'KRYPTO $(ULM_HOOK_NMSP)' \ + $(if $(DEBUG),-ccopt -O0) \ + -ccopt -g \ + -ccopt -std=c++20 \ + -ccopt -lcrypto \ + -ccopt -lsecp256k1 \ + -ccopt -lssl \ + -ccopt "$(ULM_KRYPTO_TARGET)" \ + $(ULM_LIB_FLAGS) \ + -ccopt -I"$(ULM_HOOKS_DIR)" \ + -ccopt -DULM_LANG_ID=wasm \ + --llvm-hidden-visibility \ + --llvm-kompile-type $(ULM_WASM_TYPE) \ + --llvm-kompile-output "$(ULM_WASM_OUT)" \ + -O2 \ + -I "$(ULM_HOOKS_DIR)" \ + -I "$(ULM_KRYPTO_DIR)/plugin" \ + -v \ + $(ULM_WASM_MAIN) \ + --md-selector "$(ULM_WASM_SEL)" \ + --main-module ULM-WASM \ + --syntax-module ULM-WASM-SYNTAX \ + --emit-json \ + $(if $(DEBUG),--debug) \ + -o $(ULM_WASM_DIR) + $(if $(ULM_TEST),,cp "$(ULM_WASM_DIR)/$(ULM_WASM_OUT)" "$(ULM_LIB_DIR)") + +.PHONY: ulm-wasm +ulm-wasm: $(ULM_WASM_TARGET) + +### ULM Wasm Contract Compiler + +$(ULM_WASM_COMPILER_TARGET): $(ULM_WASM_TARGET) + $(CXX) "$(ULM_HOOKS_DIR)/emit_contract_bytes.cpp" \ + -I "$(ULM_KF_INCLUDE_DIR)" \ + -I "$(ULM_KF_INCLUDE_DIR)/kllvm" \ + -std=c++20 \ + -DULM_LANG_ID=wasm \ + -Wno-return-type-c-linkage \ + -L"$(ULM_LIB_DIR)" \ + -lulmkllvm \ + -lkwasm \ + -o "$(ULM_WASM_COMPILER_TARGET)" + +.PHONY: ulm-contract-compiler +ulm-contract-compiler: $(ULM_WASM_COMPILER_TARGET) + # Testing # ------- @@ -95,6 +279,14 @@ proof_tests:=$(wildcard tests/proofs/*-spec.k) test-prove: $(proof_tests:=.prove) +### Rust tests + +rust-tests: erc20-rust-tests +.PHONY: rust-tests + +erc20-rust-tests: tests/ulm/erc20/rust/src/*.rs tests/ulm/erc20/rust/Cargo.* + cd tests/ulm/erc20/rust && RUST_BACKTRACE=1 cargo test +.PHONY: erc20-rust-tests # Analysis # -------- diff --git a/README.md b/README.md index 0cd181456..b5a99cda5 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,20 @@ make build To only build a specific backend, you can do `make build-llvm` or `make build-haskell`. +#### ULM-Integrated Wasm Building + +To locally build the ULM-integrated version of the semantics, run: + +```sh +make ULM_TEST=1 ulm-wasm +``` + +To build the remote ULM-integrated version of the semantics, run: + +```sh +make ulm-wasm +``` + ### Media and documents The `media/` directory contains presentations and reports about about KWasm. @@ -163,6 +177,39 @@ The target `test` contains all the currently passing tests. make test ``` +### ULM-Integrated Wasm Local Testing + +To execute the Wasm VM locally, you can use the `wasm` Poetry script from the repo root as follows: + +```sh +poetry -C pykwasm run wasm [-cellname:sort=cellvalue...] +``` + +For example, after locally building the ULM-integrated Wasm, the local build of the ULM-integrated Wasm can be executed as follows: + +```sh +poetry -C pykwasm run wasm ./build/wasm pykwasm/src/tests/integration/binary/basic-features.wat -gas:Int=0 -create:Bool=false -entry:String=init +``` + +### ULM-Integrated Wasm Remote Testing + +To execute the Wasm VM remotely, you need to build the ULM by running: + +```sh +make ulm-build +``` + +Then, you can start the ULM locally and load the Wasm VM into it by running: + +```sh +./scripts/run-dev-ulm & +./scripts/ulm-load-lang ./build/lib/libwasm.so +``` + +Then, you can invoke Wasm programs by doing the following: + +**TODO** + Resources --------- diff --git a/pykwasm/pyproject.toml b/pykwasm/pyproject.toml index a4f1dd7f8..ce4ba9276 100644 --- a/pykwasm/pyproject.toml +++ b/pykwasm/pyproject.toml @@ -11,6 +11,7 @@ authors = [ ] [tool.poetry.scripts] +wasm = "pykwasm.run_wasm:main" wasm2kast = "pykwasm.wasm2kast:main" kwasm = "pykwasm.scripts.kwasm:main" kwasm-convert = "pykwasm.scripts.convert:main" @@ -65,6 +66,7 @@ disallow_untyped_defs = true # TODO fix type errors exclude = [ 'src/pykwasm/wasm2kast\.py', + 'src/pykwasm/run_wasm\.py', 'src/wasm/*', 'src/tests/unit/test_wasm2kast\.py', ] diff --git a/pykwasm/src/pykwasm/kdist/plugin.py b/pykwasm/src/pykwasm/kdist/plugin.py index 97d1f9554..62d96f1fd 100644 --- a/pykwasm/src/pykwasm/kdist/plugin.py +++ b/pykwasm/src/pykwasm/kdist/plugin.py @@ -30,8 +30,16 @@ def __init__(self, kompile_args: Callable[[Path], Mapping[str, Any]]): self._kompile_args = kompile_args def build(self, output_dir: Path, deps: dict[str, Path], args: dict[str, Any], verbose: bool) -> None: + llvm_proof_hint_debugging = bool(args.get('llvm-proof-hint-debugging', '')) + llvm_proof_hint_instrumentation = bool(args.get('llvm-proof-hint-instrumentation', '')) kompile_args = self._kompile_args(deps['wasm-semantics.source']) - kompile(output_dir=output_dir, verbose=verbose, **kompile_args) + kompile( + output_dir=output_dir, + verbose=verbose, + llvm_proof_hint_debugging=llvm_proof_hint_debugging, + llvm_proof_hint_instrumentation=llvm_proof_hint_instrumentation, + **kompile_args, + ) def context(self) -> dict[str, str]: return {'k-version': k_version().text} diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/auxil.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/auxil.md index 1ed01b680..2374dcf8c 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/auxil.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/auxil.md @@ -24,7 +24,7 @@ module WASM-AUXIL #clearConfig => .K ... _ => .Int _ => .ValStack - _ => .Map + _ => .List _ => .Bag _ => .Map _ => 0 @@ -34,8 +34,7 @@ module WASM-AUXIL _ => .Bag _ => 0 _ => .Bag - _ => 0 - _ => .Bag + _ => .List _ => 0 _ => .Bag _ => 0 diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md index a9a27dc59..a83d11f14 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md @@ -430,7 +430,8 @@ The operator `#assertLocal`/`#assertGlobal` operators perform a check for a loca ```k rule #assertLocal INDEX VALUE _ => .K ... - ... INDEX |-> VALUE ... + LOCALS + requires LOCALS[INDEX] ==K VALUE rule #assertGlobal TFIDX VALUE _ => .K ... CUR @@ -569,18 +570,12 @@ This checks that the last allocated memory has the given size and max value. CUR IDS - #ContextLookup(IDS, TFIDX) |-> ADDR + ListItem(ADDR) ... - - - ADDR - MAX - SIZE - ... - - ... - + MEMS + requires ADDR #assertMemoryData (KEY , VAL) MSG => #assertMemoryData CUR (KEY, VAL) MSG ... CUR @@ -588,18 +583,12 @@ This checks that the last allocated memory has the given size and max value. rule #assertMemoryData MODIDX (KEY , VAL) _MSG => .K ... MODIDX - 0 |-> ADDR + ListItem(ADDR) ... - - - ADDR - BM - ... - - ... - - requires #getRange(BM, KEY, 1) ==Int VAL + MEMS + requires ADDR Mod + ``` + +2. In the remote ULM-integrated VM case, a specialized, hooked byte decoder is used. + + ```remote + syntax ModuleDecl ::= decodePgm(Bytes) [function, hook(ULM.decode)] + ``` + +Configuration +------------- + +Here we define the initial configuration. +Note that the default status code indicates an internal error; this is used defensively, since if we ever get stuck, an error will always be indicated. +Similarly, we define a default null output which may indicate internal errors. + +```k + syntax OutputData ::= "NO_OUTPUT" + | Bytes + + configuration + + $PGM:PgmEncoding + + $CREATE:Bool + $GAS:Int + EVMC_INTERNAL_ERROR + NO_OUTPUT +``` + +A special configuration cell is added in the local case to support VM initialization. + +```local + $ENTRY:String +``` + + +```k + +``` + +Passing Control +--------------- + +The embedder loads the module to be executed and then resolves the entrypoint function. +Currently, only the local Wasm VM initialization is supported. + +```local + rule PGM:PgmEncoding => #resolveCurModuleFuncExport(FUNCNAME) + FUNCNAME + .K => decodePgm(PGM) +``` + +Note that entrypoint resolution must occur _after_ the Wasm module has been loaded. +This is ensured by requiring that the `` cell is empty during resolution. + +```local + syntax Initializer ::= #resolveCurModuleFuncExport(String) + | #resolveModuleFuncExport(Int, String) + | #resolveFunc(Int, ListInt) + // ---------------------------------------------- + rule #resolveCurModuleFuncExport(FUNCNAME) => #resolveModuleFuncExport(MODIDX, FUNCNAME) + .K + MODIDX:Int + + rule #resolveModuleFuncExport(MODIDX, FUNCNAME) => #resolveFunc(FUNCIDX, FUNCADDRS) + .K + + MODIDX + ... FUNCNAME |-> FUNCIDX ... + FUNCADDRS + ... + + + rule #resolveFunc(FUNCIDX, FUNCADDRS) => .K + .K => (invoke FUNCADDRS {{ FUNCIDX }} orDefault -1 ):Instr + requires isListIndex(FUNCIDX, FUNCADDRS) +``` + +Here we handle the case when entrypoint resolution fails. + +**TODO:** Decide how to handle failure case. + +```k + // rule Init:Initializer => . [owise] +``` + +ULM Hook Behavior +----------------- + +These rules define various integration points between the ULM and our Wasm interpreter. + +**Note**: the first three rules hooks below are written with helper functions + because parse errors were encountered when writing `` literals. + +```k + syntax Int ::= #getGasLeft() [function, total] + rule [[ #getGasLeft() => Gas ]] + Gas:Int + + syntax Bytes ::= #getOutput() [function, total] + rule [[ #getOutput() => #if OutVal ==K NO_OUTPUT #then .Bytes #else {OutVal}:>Bytes #fi ]] + OutVal:OutputData + + syntax Int ::= #getStatus() [function, total] + rule [[ #getStatus() => #if OutVal ==K NO_OUTPUT #then EVMC_INTERNAL_ERROR #else Status #fi ]] + OutVal:OutputData + Status:Int + + rule getGasLeft(_) => #getGasLeft() + rule getOutput(_) => #getOutput() + rule getStatus(_) => #getStatus() +``` + +```k +endmodule +``` diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md index f4f0ffdee..1ed5780d7 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md @@ -1188,7 +1188,6 @@ They are currently supported in KWasm text files, but may be deprecated. rule #t2aInstr(#block(VT:VecType, IS:Instrs, BLOCKINFO)) => #block(VT, #t2aInstrs(IS), BLOCKINFO) - rule #t2aInstr<_>(init_local I V) => init_local I V rule #t2aInstr<_>(init_locals VS) => init_locals VS ``` diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md index 3a6b438d6..99e296fae 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md @@ -165,10 +165,9 @@ Internal Syntax module WASM-INTERNAL-SYNTAX imports WASM-DATA-INTERNAL-SYNTAX - syntax Instr ::= "init_local" Int Val - | "init_locals" ValStack - | "#init_locals" Int ValStack - // -------------------------------------------- + syntax Instr ::= "init_locals" ValStack + | "#init_locals" ValStack + // ---------------------------------------- syntax Int ::= #pageSize() [function, total] syntax Int ::= #maxMemorySize() [function, total] @@ -202,8 +201,8 @@ module WASM .K .ValStack - .Map - .Int + .List + .Int .Map .Map @@ -219,7 +218,7 @@ module WASM .Map 0 .Map - .Map + .List .Map .Map 0 @@ -258,15 +257,7 @@ module WASM 0 - - - 0 - .Int - 0 - .SparseBytes - - - 0 + .List 0 @@ -560,17 +551,15 @@ Variable Operators The various `init_local` variants assist in setting up the `locals` cell. ```k - rule init_local INDEX VALUE => .K ... - LOCALS => LOCALS [ INDEX <- VALUE ] + rule init_locals VALUES => #init_locals VALUES ... + _ => .List - rule init_locals VALUES => #init_locals 0 VALUES ... - - rule #init_locals _ .ValStack => .K ... - rule #init_locals N (VALUE : VALSTACK) - => init_local N VALUE - ~> #init_locals (N +Int 1) VALSTACK + rule #init_locals .ValStack => .K ... + rule #init_locals (VALUE : VALSTACK) + => #init_locals VALSTACK ... + ... .List => ListItem(VALUE) ``` The `*_local` instructions are defined here. @@ -581,18 +570,18 @@ The `*_local` instructions are defined here. | "#local.tee" "(" Int ")" [symbol(aLocal.tee)] // ---------------------------------------------------------------------- rule #local.get(I) => .K ... - VALSTACK => VALUE : VALSTACK - ... I |-> VALUE ... + VALSTACK => {LOCALS [ I ]}:>Val : VALSTACK + LOCALS rule #local.set(I) => .K ... VALUE : VALSTACK => VALSTACK LOCALS => LOCALS[I <- VALUE] - requires I in_keys(LOCALS) + requires I >=Int 0 andBool I #local.tee(I) => .K ... VALUE : _VALSTACK LOCALS => LOCALS[I <- VALUE] - requires I in_keys(LOCALS) + requires I >=Int 0 andBool I ` cell), and Unlike labels, only one frame can be "broken" through at a time. ```k - syntax Frame ::= "frame" Int ValTypes ValStack Map - // -------------------------------------------------- + syntax Frame ::= "frame" Int ValTypes ValStack List + // --------------------------------------------------- rule frame MODIDX' TRANGE VALSTACK' LOCAL' => .K ... VALSTACK => #take(lengthValTypes(TRANGE), VALSTACK) ++ VALSTACK' _ => LOCAL' @@ -1198,7 +1187,7 @@ The `#take` function will return the parameter stack in the reversed order, then ... VALSTACK => .ValStack - LOCAL => .Map + LOCAL MODIDX => MODIDX' FADDR @@ -1355,7 +1344,8 @@ The importing and exporting parts of specifications are dealt with in the respec ```k syntax MemoryDefn ::= #memory(limits: Limits, metadata: OptionalId) [symbol(aMemoryDefn)] syntax Alloc ::= allocmemory (OptionalId, Int, OptionalInt) - // ----------------------------------------------------------- + syntax KItem ::= memInst(mmax: OptionalInt, msize: Int, mdata: SparseBytes) + // --------------------------------------------------------------------------- rule #memory(... limits: #limitsMin(MIN), metadata: OID) => allocmemory(OID, MIN, .Int) ... requires MIN <=Int #maxMemorySize() rule #memory(... limits: #limits(MIN, MAX), metadata: OID) => allocmemory(OID, MIN, MAX) ... @@ -1367,21 +1357,10 @@ The importing and exporting parts of specifications are dealt with in the respec CUR IDS => #saveId(IDS, ID, 0) - .Map => (0 |-> NEXTADDR) + .List => ListItem(size(MEMS)) ... - NEXTADDR => NEXTADDR +Int 1 - - ( .Bag - => - NEXTADDR - MAX - MIN - ... - - ) - ... - + MEMS => MEMS ListItem(memInst(MAX, MIN, .SparseBytes)) ``` The assorted store operations take an address of type `i32` and a value. @@ -1399,36 +1378,27 @@ The value is encoded as bytes and stored at the "effective address", which is th rule #store(ITYPE:IValType, SOP, OFFSET) => ITYPE . SOP (IDX +Int OFFSET) VAL ... < ITYPE > VAL : < i32 > IDX : VALSTACK => VALSTACK - rule store { WIDTH EA VAL } => store { WIDTH EA VAL ({MEMADDRS[0] orDefault 0}:>Int) } ... + rule store { WIDTH EA VAL } => store { WIDTH EA VAL ADDR } ... CUR CUR - MEMADDRS + ListItem(ADDR) ... - requires 0 in_keys(MEMADDRS) andBool size(MEMADDRS) ==Int 1 andBool isInt(MEMADDRS[0] orDefault 0) - [preserves-definedness] rule store { WIDTH EA VAL ADDR } => .K ... - - ADDR - SIZE - DATA => #setRange(DATA, EA, VAL, #numBytes(WIDTH)) - ... - - requires (EA +Int #numBytes(WIDTH)) <=Int (SIZE *Int #pageSize()) + MEMS => MEMS [ ADDR <- #let memInst(MAX, SIZE, DATA) = MEMS[ADDR] #in memInst(MAX, SIZE, #setRange(DATA, EA, VAL, #numBytes(WIDTH))) ] + requires ADDR store { WIDTH EA _ ADDR } => trap ... - - ADDR - SIZE - ... - - requires (EA +Int #numBytes(WIDTH)) >Int (SIZE *Int #pageSize()) + MEMS + requires ADDR Int (msize(MEMS[ADDR]) *Int #pageSize()) rule ITYPE . store EA VAL => store { ITYPE EA VAL } ... rule _ . store8 EA VAL => store { i8 EA #wrap(1, VAL) } ... @@ -1460,32 +1430,23 @@ Sort `Signedness` is defined in module `BYTES`. rule ITYPE . load16_s EA:Int => load { ITYPE i16 EA Signed } ... rule i64 . load32_s EA:Int => load { i64 i32 EA Signed } ... - rule load { ITYPE WIDTH EA SIGN } => load { ITYPE WIDTH EA SIGN ({MEMADDRS[0] orDefault 0}:>Int)} ... + rule load { ITYPE WIDTH EA SIGN } => load { ITYPE WIDTH EA SIGN ADDR:Int } ... CUR CUR - MEMADDRS + ListItem(ADDR) ... - requires 0 in_keys(MEMADDRS) andBool size(MEMADDRS) ==Int 1 andBool isInt(MEMADDRS[0] orDefault 0) - [preserves-definedness] - rule load { ITYPE WIDTH EA SIGN ADDR:Int} => load { ITYPE WIDTH EA SIGN DATA } ... - - ADDR - SIZE - DATA - ... - - requires (EA +Int #numBytes(WIDTH)) <=Int (SIZE *Int #pageSize()) + rule load { ITYPE WIDTH EA SIGN ADDR:Int} => load { ITYPE WIDTH EA SIGN mdata(MEMS[ADDR]) } ... + MEMS + requires ADDR load { _ WIDTH EA _ ADDR:Int} => trap ... - - ADDR - SIZE - ... - - requires (EA +Int #numBytes(WIDTH)) >Int (SIZE *Int #pageSize()) + MEMS + requires ADDR Int (msize(MEMS[ADDR]) *Int #pageSize()) rule load { ITYPE WIDTH EA Signed DATA:SparseBytes } => #chop(< ITYPE > #signed(WIDTH, #getRange(DATA, EA, #numBytes(WIDTH)))) ... [preserves-definedness] @@ -1500,18 +1461,15 @@ The `size` operation returns the size of the memory, measured in pages. [Memory Size](https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-memory-mathsf-memory-size) ```k - rule memory.size => < i32 > SIZE ... + rule memory.size => < i32 > msize(MEMS[ADDR]) ... CUR CUR - 0 |-> ADDR + ListItem(ADDR) ... - - ADDR - SIZE - ... - + MEMS + requires ADDR ` field in the configuration to `true rule memory.grow => grow N ... < i32 > N : VALSTACK => VALSTACK - rule grow N => < i32 > SIZE ... + rule grow N => < i32 > msize(MEMS[ADDR]) ... CUR CUR - 0 |-> ADDR + ListItem(ADDR) ... - - ADDR - MAX - SIZE => SIZE +Int N - ... - - requires #growthAllowed(SIZE +Int N, MAX) + MEMS => #let memInst(MAX, SIZE, DATA) = MEMS[ADDR] #in MEMS[ADDR <- memInst(MAX, SIZE +Int N, DATA)] + requires ADDR grow N => < i32 > #unsigned(i32, -1) ... DET:Bool CUR CUR - 0 |-> ADDR + ListItem(ADDR) ... - - ADDR - MAX - SIZE - ... - - requires notBool DET - orBool notBool #growthAllowed(SIZE +Int N, MAX) + MEMS + requires ADDR CUR CUR - 0 |-> ADDR + ListItem(ADDR) ... - - ADDR - SIZE - ... - - requires N +Int D >Int SIZE *Int #pageSize() + MEMS + requires ADDR Int SIZE *Int #pageSize()) rule fillTrap N VAL D => fill N VAL D ... [owise] @@ -1610,15 +1557,11 @@ The spec states that this is really a sequence of `i32.store8` instructions, but CUR CUR - 0 |-> ADDR + ListItem(ADDR) ... - - ADDR - DATA => replaceAt(DATA, D, padRightBytes(.Bytes, N, VAL)) - ... - - requires notBool N ==Int 0 + MEMS => MEMS [ ADDR <- #let memInst(MAX, SIZE, DATA) = MEMS[ADDR] #in memInst(MAX, SIZE, replaceAt(DATA, D, padRightBytes(.Bytes, N, VAL))) ] + requires ADDR CUR CUR - 0 |-> ADDR + ListItem(ADDR) ... - - ADDR - SIZE - ... - - requires D +Int N >Int SIZE *Int #pageSize() - orBool S +Int N >Int SIZE *Int #pageSize() + MEMS + requires ADDR Int SIZE *Int #pageSize() + orBool S +Int N >Int SIZE *Int #pageSize()) rule copyTrap N S D => copy N S D ... [owise] @@ -1657,15 +1597,11 @@ performing a series of load and store operations as stated in the spec. CUR CUR - 0 |-> ADDR + ListItem(ADDR) ... - - ADDR - DATA => replaceAt(DATA, D, #getBytesRange(DATA, S, N)) - ... - - requires notBool N ==Int 0 + MEMS => MEMS [ ADDR <- #let memInst(MAX, SIZE, DATA) = MEMS[ADDR] #in memInst(MAX, SIZE, replaceAt(DATA, D, #getBytesRange(DATA, S, N))) ] + requires ADDR #data(IDX, IS, DATA) => sequenceInstrs(IS) ~> data { IDX DATA } ... - rule data { MEMIDX DSBYTES } => trap ... + rule data { 0 DSBYTES } => trap ... < i32 > OFFSET : _STACK CUR CUR - MEMIDX |-> ADDR + ListItem(ADDR) ... - - ADDR - SIZE - ... - - requires OFFSET +Int lengthBytes(DSBYTES) >Int SIZE *Int #pageSize() + MEMS + requires ADDR Int SIZE *Int #pageSize()) // For now, deal only with memory 0. - rule data { MEMIDX DSBYTES } => .K ... + rule data { 0 DSBYTES } => .K ... < i32 > OFFSET : STACK => STACK CUR CUR - MEMIDX |-> ADDR + ListItem(ADDR) ... - - ADDR - DATA => #setRange(DATA, OFFSET, Bytes2Int(DSBYTES, LE, Unsigned), lengthBytes(DSBYTES)) - ... - + MEMS => #let memInst(MAX, SIZE, DATA) = MEMS[ADDR] #in MEMS [ ADDR <- memInst(MAX, SIZE, #setRange(DATA, OFFSET, Bytes2Int(DSBYTES, LE, Unsigned), lengthBytes(DSBYTES))) ] + requires ADDR CUR IDS => #saveId(IDS, OID, 0) - .Map => 0 |-> ADDR + .List => ListItem(ADDRS[#ContextLookup(IDS', TFIDX)]) ... ... MOD |-> MODIDX ... MODIDX IDS' - ... #ContextLookup(IDS' , TFIDX) |-> ADDR ... + ADDRS ... NAME |-> TFIDX ... ... - - ADDR - MAX - SIZE - ... - - requires #limitsMatchImport(SIZE, MAX, LIM) + MEMS + requires #ContextLookup(IDS', TFIDX) Int Int] #in #limitsMatchImport(SIZE, MAX, LIM)) rule #import(MOD, NAME, #globalDesc(... id: OID, type: MUT TYP) ) => .K ... CUR diff --git a/pykwasm/src/pykwasm/run_wasm.py b/pykwasm/src/pykwasm/run_wasm.py new file mode 100644 index 000000000..0d769a01d --- /dev/null +++ b/pykwasm/src/pykwasm/run_wasm.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python3 + +""" +This library provides a translation from the Wasm binary format to Kast. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +from enum import Enum +from io import BytesIO +from pathlib import Path +from typing import TYPE_CHECKING + +from pyk.kast.inner import KSequence, KSort, KToken, Subst +from pyk.kast.manip import split_config_from +from pyk.kore.syntax import App, Assoc, MLPattern, SortApp +from pyk.ktool.krun import KRun +from wasm import instructions +from wasm.datatypes import GlobalType, MemoryType, Mutability, TableType, TypeIdx, ValType, addresses +from wasm.datatypes.element_segment import ElemModeActive, ElemModeDeclarative, ElemModePassive +from wasm.opcodes import BinaryOpcode +from wasm.parsers import parse_module + +from pykwasm import kwasm_ast as a + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import IO + + from pyk.kast import KInner + from pyk.kore.syntax import Pattern + from wasm.datatypes import ( + DataSegment, + ElementSegment, + Export, + Function, + FunctionType, + Global, + Import, + Limits, + Memory, + Module, + RefType, + StartFunction, + Table, + ) + from wasm.datatypes.element_segment import ElemMode + from wasm.instructions import BaseInstruction + + +def main(): + # read env vars + debug = 'DEBUG' in os.environ + + # check arg count + if len(sys.argv) < 3: + print('usage: [DEBUG=1] run_wasm [-cellname:sort=cellvalue...]') + sys.exit(1) + args = sys.argv[1:] + + # parse fixed args + llvm_dir = Path(args[0]) + wasm_file = Path(args[1]) + infile = open(wasm_file, 'rb') + + def build_subst_key(key_name): + return key_name.upper() + '_CELL' + + # parse extra args + config_subst = {} + extra_args = args[2:] + for arg in extra_args: + if arg[0] != '-': + raise ValueError(f'substitution argument was ill-formed: {arg!r}') + prekey_sort, val = arg[1:].split('=') + prekey, sort = prekey_sort.split(':') + key = build_subst_key(prekey) + + if key == 'k': + raise ValueError("substitution may not contain a 'k' key") + if key in config_subst: + raise ValueError(f'redundant key found in substitution map: {prekey}') + + if sort == 'String': + val = '"' + f'{val}' + '"' + config_subst[key] = KToken(val, sort) + + # parse module as binary (with fallback to textual parser) + try: + module = wasm2kast(infile) + except Exception: + proc_res = subprocess.run(['wat2wasm', wasm_file, '--output=/dev/stdout'], check=True, capture_output=True) + infile.close() + infile = BytesIO(proc_res.stdout) + module = wasm2kast(infile) + infile.close() + + # get runner + runner = KRun(llvm_dir) + + # embed parsed_module to + top_sort = KSort('GeneratedTopCell') + config_kast = runner.definition.init_config(top_sort) + symbolic_config, init_subst = split_config_from(config_kast) + init_subst['K_CELL'] = KSequence(module) + + # check substitution keys + ulm_keys = {'GAS_CELL', 'ENTRY_CELL', 'CREATE_CELL'} + if ulm_keys.issubset(init_subst.keys()) and not ulm_keys.issubset(config_subst.keys()): + raise ValueError( + f'ULM Wasm detected but required substition keys for these cells are missing: {ulm_keys - config_subst.keys()}' + ) + + # update config substitution + final_subst = init_subst | config_subst + + # apply substitution to config + config_with_module = Subst(final_subst)(symbolic_config) + + # convert the config to kore + config_kore = runner.kast_to_kore(config_with_module, top_sort) + + # monkey patch kore + patched_config_kore = PatternWriter(config_kore) + + # log input kore + if debug: + with open(wasm_file.name + '.input.kore', 'w') as f: + patched_config_kore.write(f) + + # run the config + proc_data = runner.run_process(patched_config_kore, term=True, expand_macros=False) + + # print the result + print(proc_data.stdout) + if proc_data.returncode != 0 or debug: + print(proc_data.stderr, file=sys.stderr) + proc_data.check_returncode() + + +class DepthChange(Enum): + UP = 1 + DOWN = -1 + PRINT = 0 + + +def pattern_write(pat: Pattern, output: IO[str], pretty=True) -> None: + """Serialize pattern to kore; used for monkey patch on Pattern object because default write function will blow the stack""" + + if pretty: + _up, _down, _print = DepthChange.UP, DepthChange.DOWN, DepthChange.PRINT + else: + _up, _down, _print = [''] * 3 + not_first_term = False + print_spacer = False + depth = 0 + stack = [pat] + + # TODO: fix bug with workitems order + + def push(*items): + for item in reversed(items): + if isinstance(item, tuple): + if len(item) > 1: + for subitem in reversed(item[1:]): + stack.append(subitem) + stack.append(',') + if len(item) > 0: + stack.append(item[0]) + elif isinstance(item, (str, DepthChange)): + stack.append(item) + else: + raise ValueError(f'Unexpected item type: {type(item)}') + + while len(stack) > 0: + pat = stack.pop() + if isinstance(pat, str): + if print_spacer: + if not_first_term: + output.write('\n' + depth * ' ') + not_first_term = True + print_spacer = False + output.write(pat) + elif isinstance(pat, App): + push(_print, pat.symbol, '{', pat.sorts, '}(', _up, pat.args, _down, ')') + elif isinstance(pat, Assoc): + push(_print, pat.kore_symbol(), '{}(', _up, pat.app, _down, ')') + elif isinstance(pat, MLPattern): + push(_print, pat.symbol(), '{', pat.sorts, '}(', pat.ctor_patterns, ')') + elif isinstance(pat, SortApp): + push(pat.name, '{', pat.sorts, '}') + elif isinstance(pat, DepthChange): + depth += pat.value + if pat == _print: + print_spacer = True + else: + pat.write(output) + + +class PatternWriter: + def __init__(self, pat: Pattern, pretty=False): + self.pat = pat + self.pretty = pretty + + def write(self, output: IO[str]): + if isinstance(self.pat, (App, SortApp, Assoc, MLPattern)): + pattern_write(self.pat, output, self.pretty) + else: + self.pat.write(output) + + +def debug(pat) -> str: + if isinstance(pat, str): + return pat + elif isinstance(pat, tuple): + return [debug(item) for item in pat] + elif isinstance(pat, App): + return pat.symbol + elif isinstance(pat, Assoc): + return pat.kore_symbol() + elif isinstance(pat, MLPattern): + return pat.symbol() + elif isinstance(pat, SortApp): + return pat.name + elif isinstance(pat, DepthChange): + return pat.name + else: + return repr(pat) + + +def wasm2kast(wasm_bytes: IO[bytes], filename=None) -> KInner: + """Returns a dictionary representing the Kast JSON.""" + ast = parse_module(wasm_bytes) + return ast2kast(ast, filename=filename) + + +def ast2kast(wasm_ast: Module, filename=None) -> KInner: + """Returns a dictionary representing the Kast JSON.""" + types = a.defns([typ(x) for x in wasm_ast.types]) + funcs = a.defns([func(x) for x in wasm_ast.funcs]) + tables = a.defns([table(x) for x in wasm_ast.tables]) + mems = a.defns([memory(x) for x in wasm_ast.mems]) + globs = a.defns([glob(x) for x in wasm_ast.globals]) + elems = a.defns([elem(x) for x in wasm_ast.elem]) + datas = a.defns([data(x) for x in wasm_ast.data]) + starts = a.defns(start(wasm_ast.start)) + imports = a.defns([imp(x) for x in wasm_ast.imports]) + exports = a.defns([export(x) for x in wasm_ast.exports]) + meta = a.module_metadata(filename=filename) + return a.module( + types=types, + funcs=funcs, + tables=tables, + mems=mems, + globs=globs, + elem=elems, + data=datas, + start=starts, + imports=imports, + exports=exports, + metadata=meta, + ) + + +######### +# Defns # +######### + + +def typ(t: FunctionType): + return a.type(func_type(t.params, t.results)) + + +def func(f: Function): + type = a.KInt(f.type_idx) + ls_list = [val_type(x) for x in f.locals] + locals = a.vec_type(a.val_types(ls_list)) + body = instrs(f.body) + return a.func(type, locals, body) + + +def table(t: Table): + ls = limits(t.type.limits) + typ = ref_type(t.type.elem_type) + return a.table(ls, typ) + + +def memory(m: Memory): + ls = limits(m.type) + return a.memory(ls) + + +def glob(g: Global): + t = global_type(g.type) + init = instrs(g.init) + return a.glob(t, init) + + +def ref_type(t: RefType): + if t is addresses.FunctionAddress: + return a.funcref + if t is addresses.ExternAddress: + return a.externref + raise ValueError(f'Invalid RefType: {t}') + + +def elem_mode(m: ElemMode) -> KInner: + if isinstance(m, ElemModeActive): + offset = instrs(m.offset) + return a.elem_active(m.table, offset) + if isinstance(m, ElemModeDeclarative): + return a.elem_declarative() + if isinstance(m, ElemModePassive): + return a.elem_passive() + raise ValueError(f'Unknown ElemMode: {m}') + + +def elem_init(init: tuple[Iterable[BaseInstruction], ...]) -> Iterable[int | None]: + def expr_to_int(expr: Iterable[BaseInstruction]) -> int | None: + # 'expr' must be a constant expression consisting of a reference instruction + assert len(expr) == 1 or len(expr) == 2 and isinstance(expr[1], instructions.End), expr + instr = expr[0] + + if isinstance(instr, instructions.RefFunc): + return instr.funcidx + if isinstance(instr, instructions.RefNull): + return None + raise ValueError(f'Invalid reference expression: {expr}') + + return [expr_to_int(e) for e in init] + + +def elem(e: ElementSegment): + typ = ref_type(e.type) + mode = elem_mode(e.mode) + init = elem_init(e.init) + return a.elem(typ, mode, init) + + +def data(d: DataSegment): + offset = instrs(d.offset) + return a.data(d.memory_idx, offset, d.init) + + +def start(s: StartFunction): + if s is None: + return [] + return [a.start(s.function_idx)] + + +def imp(i: Import): + mod_name = a.wasm_string(i.module_name) + name = a.wasm_string(i.as_name) + t = type(i.desc) + if t is TypeIdx: + desc = a.func_desc(i.desc) + elif t is GlobalType: + desc = a.global_desc(global_type(i.desc)) + elif t is TableType: + desc = a.table_desc(limits(i.desc.limits)) + elif t is MemoryType: + desc = a.memory_desc(limits(i.desc)) + return a.imp(mod_name, name, desc) + + +def export(e: Export): + name = a.wasm_string(e.name) + idx = e.desc + return a.export(name, idx) + + +########## +# Instrs # +########## + +block_id = 0 + + +def instrs(iis): + """Turn a list of instructions into KAST.""" + # We ignore `END`. + # The AST supplied by py-wasm has already parsed these and terminated the blocks. + # We also ignore `ELSE`. + # The AST supplied by py-wasm includes the statements in the else-branch as part of the `IF` instruction. + return a.instrs([instr(i) for i in iis if not i.opcode == BinaryOpcode.END and not i.opcode == BinaryOpcode.ELSE]) + + +def instr(i): + b = BinaryOpcode + global block_id + # TODO rewrite 'i.opcode == _' conditions as isinstance for better type-checking + if i.opcode == b.BLOCK: + cur_block_id = block_id + block_id += 1 + iis = instrs(i.instructions) + res = vec_type(i.result_type) + return a.BLOCK(res, iis, a.KInt(cur_block_id)) + if i.opcode == b.BR: + return a.BR(i.label_idx) + if i.opcode == b.BR_IF: + return a.BR_IF(i.label_idx) + if i.opcode == b.BR_TABLE: + return a.BR_TABLE(i.label_indices, i.default_idx) + if i.opcode == b.CALL: + return a.CALL(i.function_idx) + if i.opcode == b.CALL_INDIRECT: + return a.CALL_INDIRECT(i.type_idx) + if i.opcode == b.ELSE: + raise (ValueError('ELSE opcode: should have been filtered out.')) + if i.opcode == b.END: + raise (ValueError('End opcode: should have been filtered out.')) + if i.opcode == b.F32_CONST: + return a.F32_CONST(i.value) + if i.opcode == b.F64_CONST: + return a.F64_CONST(i.value) + if i.opcode == b.F32_REINTERPRET_I32: + raise (ValueError('Reinterpret instructions not implemented.')) + if i.opcode == b.F64_REINTERPRET_I64: + raise (ValueError('Reinterpret instructions not implemented.')) + if i.opcode == b.GET_GLOBAL: + return a.GET_GLOBAL(i.global_idx) + if i.opcode == b.GET_LOCAL: + return a.GET_LOCAL(i.local_idx) + if i.opcode == b.I32_CONST: + return a.I32_CONST(i.value) + if i.opcode == b.I64_CONST: + return a.I64_CONST(i.value) + if i.opcode == b.I32_REINTERPRET_F32: + raise (ValueError('Reinterpret instructions not implemented.')) + if i.opcode == b.I64_REINTERPRET_F64: + raise (ValueError('Reinterpret instructions not implemented.')) + if i.opcode == b.IF: + cur_block_id = block_id + block_id += 1 + thens = instrs(i.instructions) + els = instrs(i.else_instructions) + res = vec_type(i.result_type) + return a.IF(res, thens, els, a.KInt(cur_block_id)) + if i.opcode == b.F32_STORE: + return a.F32_STORE(i.memarg.offset) + if i.opcode == b.F64_STORE: + return a.F64_STORE(i.memarg.offset) + if i.opcode == b.I32_STORE: + return a.I32_STORE(i.memarg.offset) + if i.opcode == b.I64_STORE: + return a.I64_STORE(i.memarg.offset) + if i.opcode == b.I32_STORE16: + return a.I32_STORE16(i.memarg.offset) + if i.opcode == b.I64_STORE16: + return a.I64_STORE16(i.memarg.offset) + if i.opcode == b.I32_STORE8: + return a.I32_STORE8(i.memarg.offset) + if i.opcode == b.I64_STORE8: + return a.I64_STORE8(i.memarg.offset) + if i.opcode == b.I64_STORE32: + return a.I64_STORE32(i.memarg.offset) + if i.opcode == b.F32_LOAD: + return a.F32_LOAD(i.memarg.offset) + if i.opcode == b.F64_LOAD: + return a.F64_LOAD(i.memarg.offset) + if i.opcode == b.I32_LOAD: + return a.I32_LOAD(i.memarg.offset) + if i.opcode == b.I64_LOAD: + return a.I64_LOAD(i.memarg.offset) + if i.opcode == b.I32_LOAD16_S: + return a.I32_LOAD16_S(i.memarg.offset) + if i.opcode == b.I32_LOAD16_U: + return a.I32_LOAD16_U(i.memarg.offset) + if i.opcode == b.I64_LOAD16_S: + return a.I64_LOAD16_S(i.memarg.offset) + if i.opcode == b.I64_LOAD16_U: + return a.I64_LOAD16_U(i.memarg.offset) + if i.opcode == b.I32_LOAD8_S: + return a.I32_LOAD8_S(i.memarg.offset) + if i.opcode == b.I32_LOAD8_U: + return a.I32_LOAD8_U(i.memarg.offset) + if i.opcode == b.I64_LOAD8_S: + return a.I64_LOAD8_S(i.memarg.offset) + if i.opcode == b.I64_LOAD8_U: + return a.I64_LOAD8_U(i.memarg.offset) + if i.opcode == b.I64_LOAD32_S: + return a.I64_LOAD32_S(i.memarg.offset) + if i.opcode == b.I64_LOAD32_U: + return a.I64_LOAD32_U(i.memarg.offset) + if i.opcode == b.LOOP: + cur_block_id = block_id + block_id += 1 + iis = instrs(i.instructions) + res = vec_type(i.result_type) + return a.LOOP(res, iis, a.KInt(cur_block_id)) + if i.opcode == b.SET_GLOBAL: + return a.SET_GLOBAL(i.global_idx) + if i.opcode == b.SET_LOCAL: + return a.SET_LOCAL(i.local_idx) + if i.opcode == b.TEE_LOCAL: + return a.TEE_LOCAL(i.local_idx) + if isinstance(i, instructions.RefFunc): + return a.REF_FUNC(i.funcidx) + if isinstance(i, instructions.RefNull): + if i.reftype is addresses.FunctionAddress: + return a.REF_NULL('func') + if i.reftype is addresses.ExternAddress: + return a.REF_NULL('extern') + raise ValueError(f'Unknown heap type: {i}, {i.reftype}') + if isinstance(i, instructions.TableGet): + return a.TABLE_GET(i.tableidx) + if isinstance(i, instructions.TableSet): + return a.TABLE_SET(i.tableidx) + if isinstance(i, instructions.TableInit): + return a.TABLE_INIT(i.tableidx, i.elemidx) + if isinstance(i, instructions.ElemDrop): + return a.ELEM_DROP(i.elemidx) + if isinstance(i, instructions.TableCopy): + return a.TABLE_COPY(i.tableidx1, i.tableidx2) + if isinstance(i, instructions.TableGrow): + return a.TABLE_GROW(i.tableidx) + if isinstance(i, instructions.TableSize): + return a.TABLE_SIZE(i.tableidx) + if isinstance(i, instructions.TableFill): + return a.TABLE_FILL(i.tableidx) + + # Catch all for operations without direct arguments. + return eval('a.' + i.opcode.name) + + +######## +# Data # +######## + + +def val_type(t: ValType): + if t == ValType.i32: + return a.i32 + if t == ValType.i64: + return a.i64 + if t == ValType.f32: + return a.f32 + if t == ValType.f64: + return a.f64 + if t == ValType.externref: + return a.externref + if t == ValType.funcref: + return a.funcref + raise ValueError(f'Unknown value type: {t}') + + +def vec_type(ts: Iterable[ValType]): + _ts = [val_type(x) for x in ts] + return a.vec_type(a.val_types(_ts)) + + +def func_type(params, results): + pvec = vec_type(params) + rvec = vec_type(results) + return a.func_type(pvec, rvec) + + +def limits(l: Limits): + return (l.min, l.max) + + +def global_type(t: GlobalType): + vt = val_type(t.valtype) + if t.mut is Mutability.const: + return a.global_type(a.MUT_CONST, vt) + return a.global_type(a.MUT_VAR, vt) diff --git a/scripts/compile-contract b/scripts/compile-contract new file mode 100755 index 000000000..5fc41e1ad --- /dev/null +++ b/scripts/compile-contract @@ -0,0 +1,4 @@ +#!/bin/bash +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +LD_LIBRARY_PATH="$SCRIPT_DIR/../build/lib:$LD_LIBRARY_PATH" "$SCRIPT_DIR/../build/ulm-contract-compiler" diff --git a/scripts/run-dev-ulm b/scripts/run-dev-ulm new file mode 100755 index 000000000..3f72c6a85 --- /dev/null +++ b/scripts/run-dev-ulm @@ -0,0 +1,20 @@ +#!/bin/bash +set -eu +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +[ -n "${DEBUG+x}" ] && FLAGS+=" --debugger" +set +u +export LD_LIBRARY_PATH="$SCRIPT_DIR/../build/lib:$LD_LIBRARY_PATH" +set -u +FLAGS=(--dev --allow-insecure-unlock\ + --gcmode archive\ + --dev.period 5\ + --http\ + --http.addr 0.0.0.0\ + --http.corsdomain '*'\ + --http.vhosts '*'\ + --http.api debug,personal,web3,eth,net,txpool\ + --ws\ + --ws.addr 0.0.0.0\ + --ws.origins '*') +[ -n "${VERBOSE+x}" ] && set -x +"$SCRIPT_DIR/../build/geth" "${FLAGS[@]}" "$@" diff --git a/scripts/ulm-load-lang b/scripts/ulm-load-lang new file mode 100755 index 000000000..ef30352a7 --- /dev/null +++ b/scripts/ulm-load-lang @@ -0,0 +1,7 @@ +#!/bin/bash +set -eu +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +[ -n "${VERBOSE+x}" ] && set -x +[ $# -ne 1 ] && { echo "usage: $(basename $0) "; echo " NOTE: requires a running ULM server"; exit 1; } +LANG=$1 +curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"debug_loadLanguage","params":["'"$LANG"'"],"id":1}' localhost:8545 diff --git a/tests/success-llvm.out b/tests/success-llvm.out index 0eae581e8..2dee704ab 100644 --- a/tests/success-llvm.out +++ b/tests/success-llvm.out @@ -11,7 +11,7 @@ - .Map + .List .Int @@ -43,11 +43,8 @@ 0 - .MemInstCellMap + .List - - 0 - .GlobalInstCellMap diff --git a/tests/ulm/erc20/erc20.wast b/tests/ulm/erc20/erc20.wast new file mode 100644 index 000000000..e69de29bb diff --git a/tests/ulm/erc20/rust/.gitignore b/tests/ulm/erc20/rust/.gitignore new file mode 100644 index 000000000..9f970225a --- /dev/null +++ b/tests/ulm/erc20/rust/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/tests/ulm/erc20/rust/Cargo.lock b/tests/ulm/erc20/rust/Cargo.lock new file mode 100644 index 000000000..f16b8b831 --- /dev/null +++ b/tests/ulm/erc20/rust/Cargo.lock @@ -0,0 +1,131 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "erc20" +version = "0.1.0" +dependencies = [ + "bytes", + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasm-bindgen" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" diff --git a/tests/ulm/erc20/rust/Cargo.toml b/tests/ulm/erc20/rust/Cargo.toml new file mode 100644 index 000000000..36a007ac6 --- /dev/null +++ b/tests/ulm/erc20/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "erc20" +version = "0.1.0" +edition = "2021" + +#[profile.dev] +#panic = "abort" + +#[profile.release] +#panic = "abort" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" +bytes = "1.9" diff --git a/tests/ulm/erc20/rust/src/assertions.rs b/tests/ulm/erc20/rust/src/assertions.rs new file mode 100644 index 000000000..e83a5a14c --- /dev/null +++ b/tests/ulm/erc20/rust/src/assertions.rs @@ -0,0 +1,15 @@ + +use crate::ulm; + +pub fn fail(msg: &str) -> ! { + ulm::failWrapper(msg); +} + +#[macro_export] +macro_rules! require { + ( $cond:expr , $msg:expr ) => { + if ! $cond { + fail($msg); + } + } +} diff --git a/tests/ulm/erc20/rust/src/decoder.rs b/tests/ulm/erc20/rust/src/decoder.rs new file mode 100644 index 000000000..5bff0462e --- /dev/null +++ b/tests/ulm/erc20/rust/src/decoder.rs @@ -0,0 +1,114 @@ +// Decodes values according to the solidity ABI: +// https://docs.soliditylang.org/en/latest/abi-spec.html +// +// To decode, first implement the Decodable trait for the types you want to +// decode. Then create the decoder. Note that, if you want to decode n values, +// you have to provide the types for all n values in a tuple-based list that +// ends with (). +// +// let decoder: Decoder<(Type_1, (Type_2, (..., (Type_n, ())...)))> +// +// Then you can decode the actual values. Note that decoding a value provides +// a new decoder, which you must use in order to decode the subsequent values. +// +// let (value_1, decoder) = decoder.decode(); +// let (value_2, decoder) = decoder.decode(); +// ... +// let (value_n, decoder) = decoder.decode(); +// +// At the end, it's good practice to check that you have decoded everything +// you wanted. The following will not compile if decoding didn't finish. +// +// decoder.check_done(); + +use bytes::{Bytes, Buf}; +use core::marker::PhantomData; + +use crate::assertions::fail; +use crate::require; +use crate::unsigned::U256; +use crate::encoder::EncodingType; + +pub trait Decodable { + fn encoding_type() -> EncodingType; + fn head_size() -> usize; + fn decode(bytes: Bytes) -> Self; +} + +impl Decodable for String { + fn encoding_type() -> EncodingType { + EncodingType::VariableSize + } + fn head_size() -> usize { + 32 + } + fn decode(bytes: Bytes) -> Self { + let decoded = String::from_utf8(bytes.chunk().to_vec()); + match decoded { + Ok(v) => v, + Err(_) => fail("utf8 decoding error"), + } + } +} + +pub struct Decoder<'a, T> { + phantom: PhantomData<&'a T>, + buffer: Bytes, + next_value_head: usize, +} + +impl<'a, T> Decoder<'a, T> { + pub fn from_buffer(buffer: Bytes) -> Decoder<'a, T> { + Decoder { + phantom: PhantomData, + buffer, + next_value_head: 0, + } + } +} + +impl<'a> Decoder<'a, ()> { + pub fn check_done(&self) { + } +} + +impl<'a, S, T> Decoder<'a, (S, T)> + where S:Decodable +{ + pub fn decode(self) -> (S, Decoder<'a, T>) { + let head_size = + match S::encoding_type() { + EncodingType::VariableSize => 32, + EncodingType::FixedSize => S::head_size(), + }; + let current_value_head = self.next_value_head; + let next_value_head = current_value_head + head_size; + let head = self.buffer.slice(current_value_head .. next_value_head); + let encoded_value = + match S::encoding_type() { + EncodingType::FixedSize => head, + EncodingType::VariableSize => { + let value_start_u256 = U256::decode(head); + let value_start: usize = match value_start_u256.try_into() { + Ok(v) => v, + Err(s) => fail(s), + }; + require!(value_start + 32 <= self.buffer.len(), "Value index out of range"); + let value_length_u256 = U256::decode(self.buffer.slice(value_start .. value_start + 32)); + let value_length: usize = match value_length_u256.try_into() { + Ok(v) => v, + Err(s) => fail(s), + }; + require!(value_start + 32 + value_length < self.buffer.len(), "Value end out of range"); + self.buffer.slice(value_start + 32 .. value_start + 32 + value_length) + }, + }; + let decoded_value = S::decode(encoded_value); + let decoder = Decoder:: { + phantom: PhantomData, + buffer: self.buffer, + next_value_head, + }; + (decoded_value, decoder) + } +} diff --git a/tests/ulm/erc20/rust/src/encoder.rs b/tests/ulm/erc20/rust/src/encoder.rs new file mode 100644 index 000000000..82f0879f7 --- /dev/null +++ b/tests/ulm/erc20/rust/src/encoder.rs @@ -0,0 +1,102 @@ +// Encodes values according to the solidity ABI: +// https://docs.soliditylang.org/en/latest/abi-spec.html +// +// To encode, first implement the Encodable trait for the types you want to +// encode. Then do something like this: +// +// let mut encoder = Encoder::new(); +// encoder.add(value_1); +// encoder.add(value_2); +// ... +// encoder.add(value_n); +// let encoded = encoder.encode(); + +use bytes::{Bytes, BytesMut, BufMut, Buf}; + +use crate::unsigned::U256; + +pub enum EncodingType { + FixedSize = 1, + VariableSize = 2, +} + +pub trait Encodable { + fn encode(&self) -> (EncodingType, Bytes); +} + +impl Encodable for String { + fn encode(&self) -> (EncodingType, Bytes) { + let bytes = self.as_bytes(); + let total_bytes_length = 32 + ((bytes.len() + 31) / 32) * 32; + let mut result = BytesMut::with_capacity(total_bytes_length); + let (_, len_bytes) = U256::from_u64(bytes.len() as u64).encode(); + + result.put(len_bytes); + result.put(bytes); + for _ in result.len() .. total_bytes_length { + result.put_u8(0_u8); + } + + (EncodingType::VariableSize, result.freeze()) + } +} + +pub struct Encoder { + objects: Vec<(EncodingType, Bytes)>, +} + +impl Encoder { + pub fn new() -> Self { + Encoder { objects: Vec::new() } + } + + pub fn add(&mut self, value: &dyn Encodable) { + self.objects.push(value.encode()); + } + + pub fn encode(&self) -> Bytes { + let head_size = self.head_size(); + let tail_size = self.tail_size(); + let mut prefix = BytesMut::with_capacity(head_size + tail_size); + let mut suffix = BytesMut::with_capacity(tail_size); + for (encoding_type, bytes) in self.objects.iter() { + match encoding_type { + EncodingType::FixedSize => prefix.extend_from_slice(bytes), + EncodingType::VariableSize => { + let before_size = head_size + suffix.len(); + let (_, prefix_chunk) = U256::from_u64(before_size as u64).encode(); + prefix.put(prefix_chunk); + suffix.put(bytes.chunk()); + }, + } + } + prefix.put(suffix); + prefix.freeze() + } + + fn head_size(&self) -> usize { + let mut size = 0_usize; + for (encoding_type, bytes) in self.objects.iter() { + let current_size = + match encoding_type { + EncodingType::FixedSize => bytes.len(), + EncodingType::VariableSize => 32, + }; + size += current_size; + } + size + } + + fn tail_size(&self) -> usize { + let mut size = 0_usize; + for (encoding_type, bytes) in self.objects.iter() { + let current_size = + match encoding_type { + EncodingType::FixedSize => 0, + EncodingType::VariableSize => bytes.len(), + }; + size += current_size; + } + size + } +} diff --git a/tests/ulm/erc20/rust/src/encoding_tests.rs b/tests/ulm/erc20/rust/src/encoding_tests.rs new file mode 100644 index 000000000..12fa41927 --- /dev/null +++ b/tests/ulm/erc20/rust/src/encoding_tests.rs @@ -0,0 +1,78 @@ + +#[cfg(test)] +mod encoding_tests { + use crate::encoder::*; + use crate::decoder::*; + use crate::unsigned::*; + + #[test] + fn empty_encoding() { + let encoder = Encoder::new(); + let bytes = encoder.encode(); + assert_eq!(0, bytes.len()); + let decoder: Decoder<()> = Decoder::from_buffer(bytes); + decoder.check_done(); + } + + #[test] + fn u8_encoding() { + let mut encoder = Encoder::new(); + encoder.add(&Unsigned::<1>::from_u64(7)); + let bytes = encoder.encode(); + assert_eq!(32, bytes.len()); + let decoder: Decoder<(Unsigned<1>, ())> = Decoder::from_buffer(bytes); + let (value, decoder) = decoder.decode(); + assert_eq!(7, value.try_to_u64().unwrap()); + decoder.check_done(); + } + + #[test] + fn u256_encoding() { + let mut encoder = Encoder::new(); + encoder.add(&U256::from_u64(123456789)); + let bytes = encoder.encode(); + assert_eq!(32, bytes.len()); + let decoder: Decoder<(U256, ())> = Decoder::from_buffer(bytes); + let (value, decoder) = decoder.decode(); + assert_eq!(123456789, value.try_to_u64().unwrap()); + decoder.check_done(); + } + + #[test] + fn string_encoding() { + let mut encoder = Encoder::new(); + encoder.add(&("Hello world".to_string())); + let bytes = encoder.encode(); + assert_eq!(32 * 3, bytes.len()); + let decoder: Decoder<(String, ())> = Decoder::from_buffer(bytes); + let (value, decoder) = decoder.decode(); + assert_eq!("Hello world", value); + decoder.check_done(); + } + + #[test] + fn multi_encoding() { + let mut encoder = Encoder::new(); + encoder.add(&Unsigned::<1>::from_u64(7)); + encoder.add(&("Hello".to_string())); + encoder.add(&U160::from_u64(123456789)); + encoder.add(&("world".to_string())); + encoder.add(&U256::from_u64(987654321)); + let bytes = encoder.encode(); + assert_eq!(32 * 9, bytes.len()); + + let decoder: Decoder<(Unsigned::<1>, (String, (U160, (String, (U256, ())))))> + = Decoder::from_buffer(bytes); + let (value_u8, decoder) = decoder.decode(); + assert_eq!(7, value_u8.try_to_u64().unwrap()); + let (value_str1, decoder) = decoder.decode(); + assert_eq!("Hello", value_str1); + let (value_u160, decoder) = decoder.decode(); + assert_eq!(123456789, value_u160.try_to_u64().unwrap()); + let (value_str2, decoder) = decoder.decode(); + assert_eq!("world", value_str2); + let (value_u256, decoder) = decoder.decode(); + assert_eq!(987654321, value_u256.try_to_u64().unwrap()); + decoder.check_done(); + } +} diff --git a/tests/ulm/erc20/rust/src/lib.rs b/tests/ulm/erc20/rust/src/lib.rs new file mode 100644 index 000000000..7a597c22b --- /dev/null +++ b/tests/ulm/erc20/rust/src/lib.rs @@ -0,0 +1,11 @@ +mod assertions; +mod decoder; +mod encoder; +mod predicate; +mod storage; +mod unsigned; +mod ulm; + +mod encoding_tests; +mod storage_tests; +mod unsigned_tests; diff --git a/tests/ulm/erc20/rust/src/predicate.rs b/tests/ulm/erc20/rust/src/predicate.rs new file mode 100644 index 000000000..9fdd9b735 --- /dev/null +++ b/tests/ulm/erc20/rust/src/predicate.rs @@ -0,0 +1 @@ +pub trait Satisfied {} diff --git a/tests/ulm/erc20/rust/src/storage.rs b/tests/ulm/erc20/rust/src/storage.rs new file mode 100644 index 000000000..34e654164 --- /dev/null +++ b/tests/ulm/erc20/rust/src/storage.rs @@ -0,0 +1,106 @@ +// SingleChunkStorage is a class which makes it easy to work with storage +// for objects that fit in 32 bytes. Accessing storage will crash (fail) if +// the stored bytes cannot be converted to the value type. +// +// Let's say you want to access a storage object under the name N, with +// key (K1, K2, ..., Kn) and with type T. You need the following: +// * K1 ..., Kn must implement Encodable +// * T must implement TryFrom and Into +// +// Then you can build the storage object like this: +// +// let mut builder = SingleChunkStorageBuilder::::new(api, hooks_api, &("Storage name".to_string())) +// builder.add_arg(K1); +// builder.add_arg(K2); +// ... +// builder.add_arg(Kn); +// let mut storage = builder.build(); +// +// In order to set the storage value, do this: +// +// storage.set(&my_value); +// +// In order to get the stored value, do this: +// +// let my_value = storage.get(); + +use core::marker::PhantomData; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::convert::Into; +use std::rc::Rc; + +use crate::assertions::fail; +use crate::encoder::Encodable; +use crate::encoder::Encoder; +use crate::unsigned::U256; +use crate::ulm; + +pub struct SingleChunkStorage<'a, ValueType> + where + ValueType: Into + TryFrom, +{ + phantom_value: PhantomData<&'a ValueType>, + api: Rc>, + fingerprint: U256, +} + +impl<'a, ValueType> SingleChunkStorage<'a, ValueType> + where + ValueType: Into + TryFrom, +{ + pub fn new(api: Rc>, fingerprint: U256) -> Self { + SingleChunkStorage:: { phantom_value: PhantomData, api, fingerprint } + } + + pub fn set(&mut self, value: ValueType) { + let converted: U256 = value.into(); + ulm::set_account_storage(&mut *self.api.borrow_mut(), &self.fingerprint, &converted); + } + + pub fn get(&self) -> ValueType { + let u256 = ulm::get_account_storage(&*self.api.borrow(), &self.fingerprint); + match u256.try_into() { + Ok(v) => v, + Err(_) => fail("Conversion from U256 failed for storage"), + } + } +} + +pub struct SingleChunkStorageBuilder<'a, ValueType> + where + ValueType: Into + TryFrom, +{ + phantom_value: PhantomData<&'a ValueType>, + api: Rc>, + encoder: Encoder, +} + +impl<'a, ValueType> SingleChunkStorageBuilder<'a, ValueType> + where + ValueType: Into + TryFrom, +{ + pub fn new(api: Rc>, name: &String) -> Self { + let mut encoder = Encoder::new(); + encoder.add(name); + Self::from_encoder(api, encoder) + } + + fn from_encoder(api: Rc>, encoder: Encoder) -> Self { + SingleChunkStorageBuilder:: { + phantom_value: PhantomData, + api, + encoder, + } + } + + pub fn add_arg(&mut self, arg: &dyn Encodable) { + self.encoder.add(arg); + } + + pub fn build(&mut self) -> SingleChunkStorage { + let bytes = self.encoder.encode(); + let fingerprint = ulm::keccak_hash_int(&*self.api.borrow(), &bytes); + SingleChunkStorage::new(self.api.clone(), fingerprint) + } +} diff --git a/tests/ulm/erc20/rust/src/storage_tests.rs b/tests/ulm/erc20/rust/src/storage_tests.rs new file mode 100644 index 000000000..bba924984 --- /dev/null +++ b/tests/ulm/erc20/rust/src/storage_tests.rs @@ -0,0 +1,96 @@ +#[cfg(test)] +mod encoding_tests { + use crate::storage::*; + use crate::ulm; + use crate::unsigned::*; + + #[test] + fn read_value_not_set() { + let api = ulm::mock::UlmMock::new(); + let mut builder = SingleChunkStorageBuilder::::new(api, &("my_storage".to_string())); + + let storage = builder.build(); + let value: U256 = storage.get(); + + assert_eq!(U256::from_u64(0), value); + } + + #[test] + fn write_read_u256() { + let api = ulm::mock::UlmMock::new(); + let mut builder = SingleChunkStorageBuilder::::new(api, &("my_storage".to_string())); + + let mut storage = builder.build(); + storage.set(U256::from_u64(123456789)); + let value: U256 = storage.get(); + + assert_eq!(U256::from_u64(123456789), value); + } + + #[test] + fn write_read_u8() { + let api = ulm::mock::UlmMock::new(); + let mut builder = SingleChunkStorageBuilder::>::new(api, &("my_storage".to_string())); + + let mut storage = builder.build(); + storage.set(Unsigned::<1>::from_u64(123)); + let value: Unsigned<1> = storage.get(); + + assert_eq!(Unsigned::<1>::from_u64(123), value); + } + + #[test] + fn write_read_args() { + let api = ulm::mock::UlmMock::new(); + + let mut builder = SingleChunkStorageBuilder::::new(api, &("my_storage".to_string())); + + builder.add_arg(&U256::from_u64(5)); + + let mut storage = builder.build(); + storage.set(U256::from_u64(123456789)); + let value: U256 = storage.get(); + + assert_eq!(U256::from_u64(123456789), value); + } + + #[test] + fn no_confusion() { + let api = ulm::mock::UlmMock::new(); + + let mut builder1 = SingleChunkStorageBuilder::::new(api.clone(), &("my_storage".to_string())); + let mut builder2 = SingleChunkStorageBuilder::::new(api.clone(), &("my_storage1".to_string())); + let mut builder3 = SingleChunkStorageBuilder::::new(api.clone(), &("my_storage".to_string())); + let mut builder4 = SingleChunkStorageBuilder::::new(api.clone(), &("my_storage".to_string())); + let mut builder5 = SingleChunkStorageBuilder::::new(api, &("my_storage".to_string())); + + builder3.add_arg(&U256::from_u64(3)); + builder4.add_arg(&U256::from_u64(4)); + builder5.add_arg(&U256::from_u64(3)); + builder5.add_arg(&U256::from_u64(4)); + + let mut storage1 = builder1.build(); + let mut storage2 = builder2.build(); + let mut storage3 = builder3.build(); + let mut storage4 = builder4.build(); + let mut storage5 = builder5.build(); + + storage1.set(U256::from_u64(1)); + storage2.set(U256::from_u64(2)); + storage3.set(U256::from_u64(3)); + storage4.set(U256::from_u64(4)); + storage5.set(U256::from_u64(5)); + + let value1: U256 = storage1.get(); + let value2: U256 = storage2.get(); + let value3: U256 = storage3.get(); + let value4: U256 = storage4.get(); + let value5: U256 = storage5.get(); + + assert_eq!(U256::from_u64(1), value1); + assert_eq!(U256::from_u64(2), value2); + assert_eq!(U256::from_u64(3), value3); + assert_eq!(U256::from_u64(4), value4); + assert_eq!(U256::from_u64(5), value5); + } +} diff --git a/tests/ulm/erc20/rust/src/ulm.rs b/tests/ulm/erc20/rust/src/ulm.rs new file mode 100644 index 000000000..d1b1ed3ad --- /dev/null +++ b/tests/ulm/erc20/rust/src/ulm.rs @@ -0,0 +1,153 @@ +use bytes::{Bytes, Buf}; + +use crate::unsigned::U256; + +#[cfg(not(test))] +extern "C" { + // key and value must have a length of exactly 32. + #[allow(non_snake_case)] + pub fn GetAccountStorage(key: *const u8, value: *mut u8); + + // key and value must have a length of exactly 32. + #[allow(non_snake_case)] + pub fn SetAccountStorage(key: *const u8, value: *const u8); + + #[allow(dead_code)] + pub fn fail(msg: *const u8, msg_len: usize) -> !; + + // result must have a length of exactly 32. + pub fn keccakHash(msg: *const u8, msg_len: usize, result: *mut u8); + +} + +#[cfg(test)] +pub mod overrides { + #[no_mangle] + pub extern "C" fn fail(_msg: *const u8, _msg_len: usize) -> ! { + panic!("fail called"); + } +} + +#[cfg(test)] +#[allow(non_snake_case)] +pub fn failWrapper(msg: &str) -> ! { + panic!("{}", msg); +} + +#[cfg(not(test))] +#[allow(non_snake_case)] +pub fn failWrapper(msg: &str) -> ! { + let msg_bytes = msg.as_bytes(); + unsafe { fail(msg_bytes.as_ptr(), msg_bytes.len()); } +} + +pub trait Ulm { + fn get_account_storage(&self, key: &[u8; 32], value: &mut [u8; 32]); + fn set_account_storage(&mut self, key: &[u8; 32], value: &[u8; 32]); + + fn keccak_hash(&self, value: &[u8], result: &mut [u8; 32]); +} + +#[cfg(not(test))] +struct UlmImpl {} + +#[cfg(not(test))] +impl Ulm for UlmImpl { + fn get_account_storage(&self, key: &[u8; 32], value: &mut [u8; 32]) { + unsafe { GetAccountStorage(key.as_ptr(), value.as_mut_ptr()); } + } + + fn set_account_storage(&mut self, key: &[u8; 32], value: &[u8; 32]) { + unsafe { SetAccountStorage(key.as_ptr(), value.as_ptr()); } + } + + fn keccak_hash(&self, value: &[u8], result: &mut [u8; 32]) { + unsafe { keccakHash(value.as_ptr(), value.len(), result.as_mut_ptr()); } + } +} + +#[cfg(test)] +pub mod mock { + use bytes::{Bytes, Buf}; + use std::cell::RefCell; + use std::collections::HashMap; + use std::rc::Rc; + + use crate::assertions::fail; + use crate::require; + use crate::ulm::Ulm; + + pub struct UlmMock { + storage: HashMap, + } + + impl UlmMock { + pub fn new() -> Rc> { + Rc::new(RefCell::new(UlmMock { storage: HashMap::new() })) + } + } + + impl Ulm for UlmMock { + fn get_account_storage(&self, key: &[u8; 32], value: &mut [u8; 32]) { + let bytes_key = Bytes::copy_from_slice(key); + match self.storage.get(&bytes_key) { + Some(v) => { + let bytes_value = v.chunk(); + require!(bytes_value.len() == 32, "unexpected value length in storage"); + value.copy_from_slice(bytes_value); + }, + None => { + for i in 0 .. value.len() { + value[i] = 0; + } + }, + } + } + + fn set_account_storage(&mut self, key: &[u8; 32], value: &[u8; 32]) { + let bytes_key = Bytes::copy_from_slice(key); + let bytes_value = Bytes::copy_from_slice(value); + self.storage.insert(bytes_key, bytes_value); + } + + fn keccak_hash(&self, value: &[u8], result: &mut [u8; 32]) { + for i in 1 .. result.len() { + result[i] = 0; + } + for i in 1 .. value.len() { + result[i % 32] ^= value[i]; + } + } + } +} + +pub fn get_account_storage(api: &dyn Ulm, key: &U256) -> U256 { + let mut key_bytes = [0_u8; 32]; + key.copy_to_array_le(&mut key_bytes); + + let mut value_bytes = [0_u8; 32]; + api.get_account_storage(&key_bytes, &mut value_bytes); + + U256::from_array_le(value_bytes) +} + +pub fn set_account_storage(api: &mut dyn Ulm, key: &U256, value: &U256) { + let mut key_bytes = [0_u8; 32]; + key.copy_to_array_le(&mut key_bytes); + + let mut value_bytes = [0_u8; 32]; + value.copy_to_array_le(&mut value_bytes); + + api.set_account_storage(&key_bytes, &value_bytes); +} + +pub fn keccak_hash(api: &dyn Ulm, value: &Bytes) -> [u8; 32] { + let mut fingerprint = [0_u8; 32]; + api.keccak_hash(value.chunk(), &mut fingerprint); + fingerprint +} + +pub fn keccak_hash_int(api: &dyn Ulm, value: &Bytes) -> U256 { + let fingerprint = keccak_hash(api, value); + U256::from_array_le(fingerprint) +} diff --git a/tests/ulm/erc20/rust/src/unsigned.rs b/tests/ulm/erc20/rust/src/unsigned.rs new file mode 100644 index 000000000..e2d95a40c --- /dev/null +++ b/tests/ulm/erc20/rust/src/unsigned.rs @@ -0,0 +1,395 @@ +// This is a suboptimal implementation of an unsigned int, which is small and +// therefore useful for testing the wasm semantics. A proper implementation +// should probably use something like ruint2::Uint<..., ...> or uint256::Uint256. + +use bytes::{Bytes, Buf}; +use core::cmp::Ordering; +use core::ops::Add; +use core::ops::Sub; + +use crate::assertions::fail; +use crate::require; +use crate::encoder::{Encodable, EncodingType, EncodingType::FixedSize}; +use crate::decoder::Decodable; +use crate::predicate::{Satisfied}; + +#[derive(Debug)] +pub struct Unsigned { + chunks: [u8; N], +} + +// pub type U72 = Unsigned<9>; +// pub type U80 = Unsigned<10>; +// pub type U88 = Unsigned<11>; +// pub type U96 = Unsigned<12>; +// pub type U104 = Unsigned<13>; +// pub type U112 = Unsigned<14>; +// pub type U120 = Unsigned<15>; +// pub type U128 = Unsigned<16>; +// pub type U136 = Unsigned<17>; +// pub type U144 = Unsigned<18>; +// pub type U152 = Unsigned<19>; +pub type U160 = Unsigned<20>; +// pub type U168 = Unsigned<21>; +// pub type U176 = Unsigned<22>; +// pub type U184 = Unsigned<23>; +// pub type U192 = Unsigned<24>; +// pub type U200 = Unsigned<25>; +// pub type U208 = Unsigned<26>; +// pub type U216 = Unsigned<27>; +// pub type U224 = Unsigned<28>; +// pub type U232 = Unsigned<29>; +// pub type U240 = Unsigned<30>; +// pub type U248 = Unsigned<31>; +pub type U256 = Unsigned<32>; + +impl Unsigned { + pub fn try_from_unsigned(value: &Unsigned) -> Result, &'static str> { + let mut chunks = [0_u8; N]; + if M <= N { + for i in 0 .. M { + chunks[i] = value.chunks[i]; + } + } else { + for i in 0 .. N { + chunks[i] = value.chunks[i]; + } + for i in N .. M { + if value.chunks[i] != 0 { + return Err("Value too large to cast"); + } + } + } + Ok (Unsigned { chunks }) + } + + pub fn from_array_le(chunks: [u8; N]) -> Unsigned { + Unsigned { chunks } + } + + pub fn from_u64(value: u64) -> Unsigned { + if 8 <= N { + let mut chunks = [0_u8; N]; + let mut to_process = value; + for i in 0 .. 8 { + chunks[i] = (to_process & 0xff) as u8; + to_process = to_process >> 8; + } + require!(to_process == 0, "Unprocessed bits in value."); + Unsigned { chunks } + } else { + match Unsigned::try_from_unsigned(&Unsigned::<8>::from_u64(value)) { + Ok(v) => v, + Err(msg) => fail(msg), + } + } + } + + pub fn copy_to_array_le(&self, chunks: &mut [u8; N]) { + chunks.copy_from_slice(&self.chunks); + } + + pub fn try_to_u64(&self) -> Result { + let useful_length = + if 8 < N { + for i in 8 .. N { + if self.chunks[i] != 0 { + return Err("Overflow when converting to u64"); + } + } + 8 + } else { + N + }; + let mut value = 0_u64; + for i in (0 .. useful_length).rev() { + value = value << 8; + value += self.chunks[i] as u64; + } + Ok(value) + } +} + +impl TryFrom<&Unsigned> for u64 { + type Error = &'static str; + fn try_from(value: &Unsigned) -> Result { + value.try_to_u64() + } +} +impl TryFrom> for u64 { + type Error = &'static str; + fn try_from(value: Unsigned) -> Result { + (&value).try_into() + } +} +impl TryFrom<&Unsigned> for usize { + type Error = &'static str; + fn try_from(value: &Unsigned) -> Result { + let value_u64: u64 = value.try_into()?; + match value_u64.try_into() { + Ok(v) => Ok(v), + Err(_) => Err("Error converting u64 to usize") + } + } +} +impl TryFrom> for usize { + type Error = &'static str; + fn try_from(value: Unsigned) -> Result { + (&value).try_into() + } +} +impl TryFrom<&Unsigned> for Unsigned { + type Error = &'static str; + fn try_from(value: &Unsigned) -> Result { + Unsigned::try_from_unsigned(value) + } +} + +#[macro_export] +macro_rules! try_from_u256 { + ( $size:expr ) => { + impl TryFrom for Unsigned<$size> + { + type Error = &'static str; + fn try_from(value: U256) -> Result { + (&value).try_into() + } + } + impl From> for U256 + where SmallerThan32<$size>: Satisfied + { + fn from(value: Unsigned<$size>) -> Self { + (&value).try_into().unwrap() + } + } + } +} +try_from_u256!(1); +try_from_u256!(2); +try_from_u256!(3); +try_from_u256!(4); +try_from_u256!(5); +try_from_u256!(6); +try_from_u256!(7); +try_from_u256!(8); +try_from_u256!(9); +try_from_u256!(10); +try_from_u256!(11); +try_from_u256!(12); +try_from_u256!(13); +try_from_u256!(14); +try_from_u256!(15); +try_from_u256!(16); +try_from_u256!(17); +try_from_u256!(18); +try_from_u256!(19); +try_from_u256!(20); +try_from_u256!(21); +try_from_u256!(22); +try_from_u256!(23); +try_from_u256!(24); +try_from_u256!(25); +try_from_u256!(26); +try_from_u256!(27); +try_from_u256!(28); +try_from_u256!(29); +try_from_u256!(30); +try_from_u256!(31); + +impl Add for &Unsigned { + type Output = Unsigned; + fn add(self, other: &Unsigned) -> Self::Output { + let mut chunks = [0_u8; N]; + let mut carry = 0_u16; + for i in 0..N { + let value = (self.chunks[i] as u16) + (other.chunks[i] as u16) + carry; + carry = value >> 8; + chunks[i] = (value & 0xff) as u8; + } + require!(carry == 0, "Addition overflow"); + Unsigned { chunks } + } +} +impl Add for Unsigned { + type Output = Unsigned; + fn add(self, other: Unsigned) -> Self::Output { + &self + &other + } +} +impl Add> for &Unsigned { + type Output = Unsigned; + fn add(self, other: Unsigned) -> Self::Output { + self + &other + } +} +impl Add<&Unsigned> for Unsigned { + type Output = Unsigned; + fn add(self, other: &Unsigned) -> Self::Output { + &self + other + } +} + +impl Sub for &Unsigned { + type Output = Unsigned; + fn sub(self, other: &Unsigned) -> Self::Output { + let mut chunks = [0_u8; N]; + let mut carry = 0_u16; + for i in 0..N { + let self_chunk = self.chunks[i] as u16; + let to_remove = other.chunks[i] as u16 + carry; + let remove_from = + if self_chunk >= to_remove { + carry = 0; + self_chunk + } else { + carry = 1; + self_chunk + 0x100 + }; + require!(remove_from >= to_remove, "Unexpected value in subtraction"); + let result = remove_from - to_remove; + require!(result <= 0xff, "Unexpected value in subtraction"); + chunks[i] = result as u8; + } + require!(carry == 0, "Subtraction overflow"); + Unsigned { chunks } + } +} +impl Sub for Unsigned { + type Output = Unsigned; + fn sub(self, other: Unsigned) -> Self::Output { + &self - &other + } +} +impl Sub> for &Unsigned { + type Output = Unsigned; + fn sub(self, other: Unsigned) -> Self::Output { + self - &other + } +} +impl Sub<&Unsigned> for Unsigned { + type Output = Unsigned; + fn sub(self, other: &Unsigned) -> Self::Output { + &self - other + } +} + +impl Ord for Unsigned { + fn cmp(&self, other: &Self) -> Ordering { + for i in (0..N).rev() { + if self.chunks[i] < other.chunks[i] { + return Ordering::Less; + } + if self.chunks[i] > other.chunks[i] { + return Ordering::Greater; + } + } + Ordering::Equal + } +} +impl PartialOrd for Unsigned { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl PartialEq for Unsigned { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} +impl Eq for Unsigned {} +impl Clone for Unsigned { + fn clone(&self) -> Self { + Unsigned { chunks: self.chunks.clone() } + } +} + +impl Encodable for U256 +{ + fn encode(&self) -> (EncodingType, Bytes) { + let mut encoded = [0_u8; 32]; + for i in 0 .. 32 { + encoded[i] = self.chunks[31 - i]; + } + (FixedSize, Bytes::copy_from_slice(&encoded)) + } +} +impl Decodable for U256 +{ + fn encoding_type() -> EncodingType { + FixedSize + } + fn head_size() -> usize { + 32 + } + fn decode(bytes: Bytes) -> Self { + let encoded = bytes.chunk(); + require!(32 == encoded.len(), "Wrong length to decode"); + let mut value: U256 = U256::from_u64(0); + for i in 0 .. 32 { + value.chunks[31 - i] = encoded[i]; + } + value + } +} +impl Encodable for Unsigned +where + SmallerThan32: Satisfied +{ + fn encode(&self) -> (EncodingType, Bytes) { + match U256::try_from_unsigned(self) { + Ok(v) => v.encode(), + Err(msg) => fail(msg), + } + } +} +impl Decodable for Unsigned +where + SmallerThan32: Satisfied +{ + fn encoding_type() -> EncodingType { + U256::encoding_type() + } + fn head_size() -> usize { + U256::head_size() + } + fn decode(bytes: Bytes) -> Self { + let value_u256 = U256::decode(bytes); + match Unsigned::::try_from_unsigned(&value_u256) { + Ok(v) => v, + Err(msg) => fail(msg), + } + } +} + +pub enum SmallerThan32 {} +impl Satisfied for SmallerThan32<1> {} +impl Satisfied for SmallerThan32<2> {} +impl Satisfied for SmallerThan32<3> {} +impl Satisfied for SmallerThan32<4> {} +impl Satisfied for SmallerThan32<5> {} +impl Satisfied for SmallerThan32<6> {} +impl Satisfied for SmallerThan32<7> {} +impl Satisfied for SmallerThan32<8> {} +impl Satisfied for SmallerThan32<9> {} +impl Satisfied for SmallerThan32<10> {} +impl Satisfied for SmallerThan32<11> {} +impl Satisfied for SmallerThan32<12> {} +impl Satisfied for SmallerThan32<13> {} +impl Satisfied for SmallerThan32<14> {} +impl Satisfied for SmallerThan32<15> {} +impl Satisfied for SmallerThan32<16> {} +impl Satisfied for SmallerThan32<17> {} +impl Satisfied for SmallerThan32<18> {} +impl Satisfied for SmallerThan32<19> {} +impl Satisfied for SmallerThan32<20> {} +impl Satisfied for SmallerThan32<21> {} +impl Satisfied for SmallerThan32<22> {} +impl Satisfied for SmallerThan32<23> {} +impl Satisfied for SmallerThan32<24> {} +impl Satisfied for SmallerThan32<25> {} +impl Satisfied for SmallerThan32<26> {} +impl Satisfied for SmallerThan32<27> {} +impl Satisfied for SmallerThan32<28> {} +impl Satisfied for SmallerThan32<29> {} +impl Satisfied for SmallerThan32<30> {} +impl Satisfied for SmallerThan32<31> {} diff --git a/tests/ulm/erc20/rust/src/unsigned_tests.rs b/tests/ulm/erc20/rust/src/unsigned_tests.rs new file mode 100644 index 000000000..ba18734af --- /dev/null +++ b/tests/ulm/erc20/rust/src/unsigned_tests.rs @@ -0,0 +1,76 @@ + +#[cfg(test)] +mod unsigned_tests { + use crate::unsigned::*; + + #[test] + fn simple_addition() { + let first = Unsigned::<1>::from_u64(2); + let second = Unsigned::<1>::from_u64(3); + let result: Unsigned<1> = first + second; + assert_eq!(result, Unsigned::from_u64(5)); + } + + #[test] + fn addition_byte_overflow() { + let result: Unsigned<2> = Unsigned::from_u64(129) + Unsigned::from_u64(128); + assert_eq!(result, Unsigned::from_u64(257)); + } + + #[test] + fn simple_subtraction() { + let result: Unsigned<1> = Unsigned::from_u64(5) - Unsigned::from_u64(3); + assert_eq!(result, Unsigned::from_u64(2)); + } + + #[test] + fn subtraction_byte_overflow() { + let result: Unsigned<2> = Unsigned::from_u64(257) - Unsigned::from_u64(128); + assert_eq!(result, Unsigned::from_u64(129)); + } + + #[test] + fn eq() { + assert_eq!(Unsigned::<1>::from_u64(5), Unsigned::from_u64(5)); + assert_ne!(Unsigned::<1>::from_u64(8), Unsigned::from_u64(5)); + } + + #[test] + fn lt() { + assert!(Unsigned::<1>::from_u64(5) < Unsigned::from_u64(8)); + assert!(!(Unsigned::<1>::from_u64(8) < Unsigned::from_u64(5))); + assert!(Unsigned::<2>::from_u64(0x105) < Unsigned::from_u64(0x108)); + assert!(!(Unsigned::<2>::from_u64(0x108) < Unsigned::from_u64(0x105))); + } + + #[test] + fn le() { + assert!(Unsigned::<1>::from_u64(5) <= Unsigned::from_u64(8)); + assert!(!(Unsigned::<1>::from_u64(8) <= Unsigned::from_u64(5))); + assert!(Unsigned::<2>::from_u64(0x105) <= Unsigned::from_u64(0x108)); + assert!(!(Unsigned::<2>::from_u64(0x108) <= Unsigned::from_u64(0x105))); + assert!(Unsigned::<2>::from_u64(0x105) <= Unsigned::from_u64(0x105)); + } + + #[test] + fn gt() { + assert!(Unsigned::<1>::from_u64(8) > Unsigned::from_u64(5)); + assert!(!(Unsigned::<1>::from_u64(5) > Unsigned::from_u64(8))); + assert!(Unsigned::<2>::from_u64(0x108) > Unsigned::from_u64(0x105)); + assert!(!(Unsigned::<2>::from_u64(0x105) > Unsigned::from_u64(0x108))); + } + + #[test] + fn ge() { + assert!(Unsigned::<1>::from_u64(8) >= Unsigned::from_u64(5)); + assert!(!(Unsigned::<1>::from_u64(5) >= Unsigned::from_u64(8))); + assert!(Unsigned::<2>::from_u64(0x108) >= Unsigned::from_u64(0x105)); + assert!(!(Unsigned::<2>::from_u64(0x105) >= Unsigned::from_u64(0x108))); + assert!(Unsigned::<2>::from_u64(0x105) >= Unsigned::from_u64(0x105)); + } + + #[test] + fn from_unsigned() { + assert_eq!(U256::from_u64(5), U256::try_from_unsigned(&U160::from_u64(5)).unwrap()); + } +}