diff --git a/.ci-config/xrpld.cfg b/.ci-config/xrpld.cfg index 7ba05ab9..258774bc 100644 --- a/.ci-config/xrpld.cfg +++ b/.ci-config/xrpld.cfg @@ -190,6 +190,7 @@ TokenEscrow SingleAssetVault LendingProtocol PermissionDelegationV1_1 +ConfidentialTransfer # This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode [voting] diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 9d0a1e85..ade74ad0 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -31,7 +31,6 @@ jobs: run: | docker run \ --detach \ - --rm \ --publish 5005:5005 \ --publish 6006:6006 \ --volume "${{ github.workspace }}/.ci-config/":"/etc/xrpld/" \ @@ -59,7 +58,7 @@ jobs: - name: Run integration tests under coverage run: | cargo llvm-cov --no-report --release \ - --features std,json-rpc,helpers,cli,websocket,integration \ + --features std,json-rpc,helpers,cli,websocket,integration,confidential-mpt \ --test integration_test \ --test cli_integration \ --test funding \ diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 8dbb9a9d..03afbb58 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -33,6 +33,18 @@ jobs: env: RUST_BACKTRACE: 1 + # confidential-mpt is not a default feature (it links the mpt-crypto + # native library via FFI), so the cMPT transaction-model unit tests and + # the mpt-crypto safe-wrapper tests are not covered by the step above. + # Run them explicitly. Kept separate from the coverage step so the + # unexercised FFI crate does not affect the unit coverage threshold. + - name: Test confidential-mpt (models + mpt-crypto wrapper) + run: | + cargo test --release --features confidential-mpt --lib + cargo test --release -p mpt-crypto + env: + RUST_BACKTRACE: 1 + - name: Test for no_std run: cargo test --release --no-default-features --features embassy-rt,core,utils,wallet,models,helpers,websocket,json-rpc diff --git a/Cargo.toml b/Cargo.toml index 1570edce..da233b8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["internal/mpt-crypto-sys", "internal/mpt-crypto"] + [package] name = "xrpl-rust" version = "1.1.0" @@ -26,6 +29,9 @@ crate-type = ["lib"] [dependencies] xrpl-rust-macros = { version = "0.1.0", path = "xrpl-rust-macros" } +# Optional: enabled by the `confidential-mpt` feature. The mpt-crypto crate +# is std-only (FFI to the C library), so the feature also pulls in `std`. +mpt-crypto = { version = "0.1.0", path = "internal/mpt-crypto", optional = true } lazy_static = "1.4.0" sha2 = { version = "0.10.2", default-features = false } @@ -139,6 +145,10 @@ websocket = [ core = ["utils"] utils = [] cli = ["dep:clap", "dep:bip39"] +# Confidential MPT (XLS-0096) support. Requires `std` because mpt-crypto links +# the native libmpt-crypto library via FFI. Adds the 5 ConfidentialMPT +# transaction models and the safe-wrapper crypto crate to the public surface. +confidential-mpt = ["std", "models", "dep:mpt-crypto"] integration = [] # Enable integration tests (requires network access) std = [ "rand/std", diff --git a/internal/mpt-crypto-sys/.gitignore b/internal/mpt-crypto-sys/.gitignore new file mode 100644 index 00000000..8ba75491 --- /dev/null +++ b/internal/mpt-crypto-sys/.gitignore @@ -0,0 +1,7 @@ +# Vendored native binaries are not committed — they're large (~40 MB across +# all platforms), and `build.rs` fetches them from the upstream GitHub +# release on first build, verified by SHA-256. +# +# Developers who want offline builds can run `scripts/fetch_upstream.sh` +# locally to populate this directory; their local copy is gitignored. +vendor/lib/ diff --git a/internal/mpt-crypto-sys/Cargo.toml b/internal/mpt-crypto-sys/Cargo.toml new file mode 100644 index 00000000..d2ba52ce --- /dev/null +++ b/internal/mpt-crypto-sys/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "mpt-crypto-sys" +version = "0.1.0" +edition = "2024" +description = "Low-level FFI bindings to the mpt-crypto C library for XLS-0096 Confidential MPTs." +license = "ISC" +repository = "https://github.com/XRPLF/xrpl-rust" +publish = false +links = "mpt-crypto" + +# If this crate is ever published, the committed binaries would exceed +# crates.io's size limit. Exclude them — consumers would have to use the +# build.rs download path or set MPT_CRYPTO_LIB_DIR. +exclude = ["vendor/lib/**"] + +[lib] +name = "mpt_crypto_sys" + +# --- build-time dependencies: HTTP, gzip, tar, checksum --- +# These compile once per target machine on the first build that has to +# fetch the upstream bundle. They are not part of the consumer's final +# binary; they exist only to run build.rs. +[build-dependencies] +ureq = { version = "2.10", default-features = false, features = ["tls"] } +flate2 = "1.0" +tar = "0.4" +sha2 = "0.10" diff --git a/internal/mpt-crypto-sys/README.md b/internal/mpt-crypto-sys/README.md new file mode 100644 index 00000000..255dd13d --- /dev/null +++ b/internal/mpt-crypto-sys/README.md @@ -0,0 +1,223 @@ +# mpt-crypto-sys + +Low-level FFI bindings to the [mpt-crypto](https://github.com/XRPLF/mpt-crypto) +C library, which implements the cryptographic primitives for +[XLS-0096 Confidential Multi-Purpose Tokens](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0096-confidential-mpt) +on the XRP Ledger. + +This is an **internal crate**. The public API lives in the sibling +`mpt-crypto` crate (safe Rust wrappers, idiomatic types, secret zeroization). + +## How the native library is resolved + +`build.rs` finds `libmpt-crypto.{so,dylib,dll}` in this priority order: + +1. **`MPT_CRYPTO_LIB_DIR` environment variable.** For air-gapped builds, + enterprise setups, or pointing at a locally-built copy. Must contain + the platform-appropriate library file. +2. **`vendor/lib//`** — populated locally by running + `scripts/fetch_upstream.sh`. **Not committed to git** (binaries are + ~40 MB across all platforms). Useful for offline builds or iterating + against a custom mpt-crypto build. +3. **Downloaded from the upstream GitHub release** (the default for fresh + clones). Runs once per clean build, cached in `OUT_DIR`, verified by the + hardcoded `BUNDLE_SHA256` constant against tampering or upstream + retagging. Supported platforms: + - `aarch64-apple-darwin` + - `x86_64-apple-darwin` + - `aarch64-unknown-linux-gnu` + - `x86_64-unknown-linux-gnu` + - `s390x-unknown-linux-gnu` + - `x86_64-pc-windows-msvc` + +The linker is pointed at whichever directory wins. `build.rs` also copies +the shared library to `target//` so tests, examples, and built +binaries find it at runtime via embedded rpath (`@loader_path` on macOS, +`$ORIGIN` on Linux) or adjacency (Windows). + +### First-build network requirement + +A fresh `cargo build` after `git clone` downloads roughly 20 MB from +`github.com/XRPLF/mpt-crypto/releases/`. Subsequent builds reuse the +cached extract in `OUT_DIR`. To skip the download (e.g., on an air-gapped +build server), either run `scripts/fetch_upstream.sh` once on a machine +with network access and copy the populated `vendor/lib/` over, or set +`MPT_CRYPTO_LIB_DIR` to a directory already containing the right binary. + +## Where each file comes from + +This crate mixes hand-written code, auto-generated code, and vendored upstream +content. Provenance: + +| Path | Source | Refreshed by | +|---|---|---| +| `src/bindings.rs` | Auto-generated by [bindgen](https://rust-lang.github.io/rust-bindgen/) from the C headers in `vendor/include/`. The first ~5 lines are a script-added banner; everything below is bindgen output verbatim. | `scripts/regenerate_bindings.sh` (requires `bindgen-cli` + `libclang`) | +| `vendor/include/secp256k1_mpt.h` | Upstream mpt-crypto release tarball: `https://github.com/XRPLF/mpt-crypto/releases/download//mpt-crypto-natives-.tar.gz` | `scripts/fetch_upstream.sh ` | +| `vendor/include/utility/mpt_utility.h` | Same upstream release tarball | `scripts/fetch_upstream.sh ` | +| `vendor/include/secp256k1.h` | [bitcoin-core/secp256k1](https://github.com/bitcoin-core/secp256k1) `include/secp256k1.h` at commit [`ea174fe`](https://github.com/bitcoin-core/secp256k1/blob/ea174fe045e1832548cd3b7090958afe9573ad2b/include/secp256k1.h) (file SHA-256 `e1717d89…d2209`). Vendored separately because the mpt-crypto release bundle doesn't include it but `secp256k1_mpt.h` `#include`s it. | One-time vendoring; refresh manually (see below) if mpt-crypto starts using newer secp256k1 APIs | +| `vendor/lib//libmpt-crypto.*` | Same upstream release tarball, per-platform binaries. **Gitignored.** | `build.rs` downloads on-demand at build time, OR `scripts/fetch_upstream.sh ` | +| `build.rs`, `Cargo.toml`, `src/lib.rs`, `tests/smoke.rs`, `scripts/*.sh`, `README.md`, `.gitignore` | Hand-written | Edit directly | + +### About `src/bindings.rs` specifically + +It is **machine-generated**. Do not edit it by hand — your changes will be +overwritten by the next run of `scripts/regenerate_bindings.sh`. The script +creates a temporary "shim" header in `vendor/include/_shim_all.h` that +`#include`s both public headers, runs bindgen against it with an allowlist +filtering down to the `mpt-crypto` and `secp256k1` symbol families, and +writes the result here. + +The committed file is what every downstream `cargo build` compiles against. +Regenerating it requires `libclang` and `bindgen-cli`, but **only the +maintainer regenerating** needs those tools — downstream consumers compile +the committed Rust source directly. + +To inspect what's available in the FFI surface without reading the file: + +```bash +# All exported functions +grep "pub fn " src/bindings.rs | awk '{print $3}' | sed 's/(.*//' | sort + +# All compile-time constants +grep "^pub const " src/bindings.rs | awk '{print $3}' | sed 's/:$//' +``` + +## Bumping the mpt-crypto version + +Upstream releases a new tag (e.g. `0.4.0`): + +```bash +cd internal/mpt-crypto-sys + +# 1. Refresh vendored headers + per-platform binaries. +scripts/fetch_upstream.sh 0.4.0 + +# 2. Update constants in build.rs: +# MPT_CRYPTO_VERSION = "0.4.0" +# BUNDLE_SHA256 = "" + +# 3. Refresh the Rust FFI declarations. +scripts/regenerate_bindings.sh + +# 4. Commit headers, bindings, and build.rs (NOT vendor/lib/, gitignored). +git add vendor/include/ src/bindings.rs build.rs +git commit -m "mpt-crypto-sys: bump to 0.4.0" +``` + +### Refreshing `vendor/include/secp256k1.h` separately + +The bitcoin-core `secp256k1.h` header isn't bundled with mpt-crypto releases, +so it's tracked independently. To refresh — typically only needed if mpt-crypto +starts depending on a newer secp256k1 API: + +```bash +# Pin to a specific commit or release tag for reproducibility. +COMMIT=ea174fe045e1832548cd3b7090958afe9573ad2b # or e.g. v0.7.1 +curl -sSL \ + "https://raw.githubusercontent.com/bitcoin-core/secp256k1/${COMMIT}/include/secp256k1.h" \ + -o vendor/include/secp256k1.h + +# Sanity-check the header parses against the rest of vendor/include/ +scripts/regenerate_bindings.sh +``` + +After that, update the `secp256k1.h` row in this README's provenance table +with the new commit SHA and file SHA-256. + +## Verifying the build linked correctly + +Two commands cover day-to-day "is it working?": + +```bash +# 1. Functional check — runs three smoke tests that exercise FFI end-to-end. +cargo test -p mpt-crypto-sys + +# 2. Linkage check — confirms the test binary references the shared lib via +# rpath (relocatable), not an absolute path. +otool -L target/debug/deps/smoke-* 2>/dev/null | grep -A1 mpt-crypto # macOS +ldd target/debug/deps/smoke-* 2>/dev/null | grep mpt-crypto # Linux +``` + +If the test command passes and the linkage check shows +`@rpath/libmpt-crypto.dylib` (macOS) or just `libmpt-crypto.so => …` (Linux), +the FFI chain is healthy. + +### Deeper inspection (when something is wrong) + +If a build succeeds but the test binary fails at runtime with "Library not +loaded" (macOS) or "error while loading shared libraries" (Linux): + +```bash +# Was the dylib copied to target// by build.rs? +ls -lh target/debug/libmpt-crypto.* + +# What rpath entries did the linker bake in? +otool -l target/debug/deps/smoke-* | grep -A2 LC_RPATH # macOS +readelf -d target/debug/deps/smoke-* | grep -E 'RPATH|RUNPATH' # Linux + +# What is dyld actually trying? Re-run the test binary directly so its +# diagnostic stderr is not swallowed by cargo: +TEST_BIN=$(ls -t target/debug/deps/smoke-* | grep -v '\.d$' | head -1) +"$TEST_BIN" +``` + +The diagnostic message lists exactly which directories were searched — +that's usually enough to identify a wrong-arch dylib, a missing copy, or a +malformed rpath. + +### Negative test — proving rpath is load-bearing + +To confirm the rpath mechanism is actually doing the work (and the dylib +isn't being found by accident via `DYLD_LIBRARY_PATH` or similar): + +```bash +TEST_BIN=$(ls -t target/debug/deps/smoke-* | grep -v '\.d$' | head -1) + +# Hide the dylib; the binary should fail to load. +mv target/debug/libmpt-crypto.dylib /tmp/.libmpt-crypto.dylib.bak +"$TEST_BIN" # expect: dyld: Library not loaded: @rpath/libmpt-crypto.dylib + +# Restore; the binary should run again. +mv /tmp/.libmpt-crypto.dylib.bak target/debug/libmpt-crypto.dylib +"$TEST_BIN" # expect: 3 passed; 0 failed +``` + +### Platform reference + +| macOS | Linux | +|---|---| +| `otool -L ` | `ldd ` | +| `otool -l \| grep LC_RPATH` | `readelf -d \| grep -E 'RPATH\|RUNPATH'` | +| `@rpath/libmpt-crypto.dylib` | `libmpt-crypto.so` | +| `@loader_path`, `@executable_path` | `$ORIGIN` | +| `dyld[…]: Library not loaded:` | `error while loading shared libraries:` | + +## Downstream build dependencies + +Because `src/bindings.rs` is committed, downstream consumers of this crate +**do not** need `libclang` or `bindgen-cli` installed. Only maintainers +regenerating bindings need those tools. + +If the native library is fetched at build time (priority 3 above), the +consumer needs network access during the first `cargo build` after a clean +checkout. Subsequent builds use the cached copy in `OUT_DIR`. + +## Why `publish = false` + +This is an internal binding crate; it has no value as a standalone library +on crates.io. Consumers should use the parent `xrpl-rust` workspace via a +path or git dependency, which routes through the safe `mpt-crypto` wrapper +crate. + +## Offline or enterprise build + +If the build machine can't reach github.com (or the download path is +otherwise undesired), point the build at a pre-fetched library directory: + +```bash +export MPT_CRYPTO_LIB_DIR=/path/to/dir/containing/libmpt-crypto.so +cargo build +``` + +The directory must contain `libmpt-crypto.{so,dylib}` (Unix) or +`mpt-crypto.dll` (Windows) matching the target triple. diff --git a/internal/mpt-crypto-sys/build.rs b/internal/mpt-crypto-sys/build.rs new file mode 100644 index 00000000..4c4164ae --- /dev/null +++ b/internal/mpt-crypto-sys/build.rs @@ -0,0 +1,212 @@ +//! Build script for mpt-crypto-sys. +//! +//! Resolves the native `libmpt-crypto.{so,dylib,dll}` in three tiers: +//! +//! 1. `MPT_CRYPTO_LIB_DIR` env var (offline / custom builds). +//! 2. `vendor/lib//` committed in this crate (git-checkout flow). +//! 3. Downloaded from the upstream GitHub release, verified by SHA-256 +//! against `BUNDLE_SHA256`, cached in `OUT_DIR`. +//! +//! After resolution, emits linker directives, rpath, and copies the shared +//! library next to the final build artifact so tests and examples find it +//! at runtime. + +use std::env; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +/// Upstream mpt-crypto release tag this crate is built against. +/// Must match the version whose headers generated `src/bindings.rs`. +const MPT_CRYPTO_VERSION: &str = "0.3.0-rc2"; + +/// SHA-256 of `mpt-crypto-natives-.tar.gz`. +/// Computed at release time; verified on every download. +/// +/// Update via `scripts/fetch_upstream.sh` which prints the new value. +const BUNDLE_SHA256: &str = "486c775b5b3c1fc18c3a9e8cbd4b458802b49eb307475df695fc899127226c0d"; + +fn main() { + let target = env::var("TARGET").expect("cargo did not set TARGET"); + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("cargo did not set OUT_DIR")); + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let (lib_filename, is_windows) = platform_lib(&target); + let lib_dir = resolve_library_dir(&target, &lib_filename, &manifest_dir, &out_dir); + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + println!("cargo:rustc-link-lib=dylib=mpt-crypto"); + + emit_rpath(&target); + copy_lib_to_output(&lib_dir.join(&lib_filename), &out_dir, is_windows); + + println!("cargo:rerun-if-env-changed=MPT_CRYPTO_LIB_DIR"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/bindings.rs"); +} + +fn platform_lib(target: &str) -> (String, bool) { + if target.contains("apple-darwin") { + ("libmpt-crypto.dylib".into(), false) + } else if target.contains("linux") { + ("libmpt-crypto.so".into(), false) + } else if target.contains("windows") { + ("mpt-crypto.dll".into(), true) + } else { + panic!( + "mpt-crypto-sys: unsupported target `{target}`. \ + Supported: *-apple-darwin, *-linux-gnu, x86_64-pc-windows-msvc." + ); + } +} + +fn resolve_library_dir( + target: &str, + lib_filename: &str, + manifest_dir: &Path, + out_dir: &Path, +) -> PathBuf { + // Priority 1: explicit override via environment variable. + if let Ok(custom) = env::var("MPT_CRYPTO_LIB_DIR") { + let path = PathBuf::from(&custom); + assert!( + path.join(lib_filename).exists(), + "MPT_CRYPTO_LIB_DIR=`{custom}` does not contain `{lib_filename}`" + ); + return path; + } + + // Priority 2: vendored in the repository. + let vendored = manifest_dir.join("vendor/lib").join(target); + if vendored.join(lib_filename).exists() { + return vendored; + } + + // Priority 3: fetch from upstream release. + let cache_dir = out_dir.join("vendor/lib").join(target); + if !cache_dir.join(lib_filename).exists() { + download_and_extract(target, &cache_dir, out_dir); + } + cache_dir +} + +fn download_and_extract(target: &str, dest: &Path, out_dir: &Path) { + let url = format!( + "https://github.com/XRPLF/mpt-crypto/releases/download/\ + {v}/mpt-crypto-natives-{v}.tar.gz", + v = MPT_CRYPTO_VERSION, + ); + let tarball = out_dir.join("mpt-crypto-natives.tar.gz"); + + println!("cargo:warning=mpt-crypto-sys: downloading {url}"); + let resp = ureq::get(&url) + .call() + .unwrap_or_else(|e| panic!("mpt-crypto-sys: download failed: {e}")); + let mut file = fs::File::create(&tarball).unwrap(); + io::copy(&mut resp.into_reader(), &mut file).unwrap(); + drop(file); + + verify_sha256(&tarball, BUNDLE_SHA256); + + fs::create_dir_all(dest).unwrap(); + let upstream = rust_to_upstream(target); + + let gz = fs::File::open(&tarball).unwrap(); + let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(gz)); + + for entry in archive.entries().unwrap() { + let mut entry = entry.unwrap(); + let path = entry.path().unwrap().to_path_buf(); + let s = path.to_string_lossy(); + let prefix_with_dot = format!("./{upstream}/"); + let prefix = format!("{upstream}/"); + if (s.starts_with(&prefix_with_dot) || s.starts_with(&prefix)) + && let Some(filename) = path.file_name() + { + entry.unpack(dest.join(filename)).unwrap(); + } + } +} + +fn verify_sha256(path: &Path, expected: &str) { + use sha2::{Digest, Sha256}; + let bytes = fs::read(path).unwrap(); + let actual = format!("{:x}", Sha256::digest(&bytes)); + assert_eq!( + actual, expected, + "mpt-crypto-sys: SHA-256 mismatch on downloaded bundle\n\ + expected: {expected}\n\ + actual: {actual}\n\ + Possible tampering, corrupted download, or MPT_CRYPTO_VERSION mismatch." + ); +} + +fn rust_to_upstream(target: &str) -> &'static str { + if target.starts_with("aarch64-apple-darwin") { + "darwin-aarch64" + } else if target.starts_with("x86_64-apple-darwin") { + "darwin-x86-64" + } else if target.starts_with("aarch64-unknown-linux-gnu") { + "linux-aarch64" + } else if target.starts_with("x86_64-unknown-linux-gnu") { + "linux-x86-64" + } else if target.starts_with("s390x-unknown-linux-gnu") { + "linux-s390x" + } else if target.starts_with("x86_64-pc-windows-msvc") { + "win32-x86-64" + } else { + panic!("mpt-crypto-sys: no upstream bundle for target `{target}`"); + } +} + +fn emit_rpath(target: &str) { + // Three search directories per platform: + // @loader_path — same directory as the binary + // (e.g. `target/debug/examples/` with dylib in `target/debug/examples/`) + // @loader_path/.. — parent directory + // (e.g. `target/debug/deps/` with dylib in `target/debug/`) + // @loader_path/../lib — installed layout (binaries in bin/, libs in lib/) + if target.contains("apple-darwin") { + println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path"); + println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path/.."); + println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path/../lib"); + } else if target.contains("linux") { + println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"); + println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/.."); + println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/../lib"); + } + // Windows has no rpath concept — DLL must be adjacent to the .exe + // (handled by copy_lib_to_output) or on %PATH%. +} + +fn copy_lib_to_output(src: &Path, out_dir: &Path, is_windows: bool) { + // OUT_DIR = target//build/-/out. + // Walk up three levels to reach target//. + let mut target_dir = out_dir.to_path_buf(); + for _ in 0..3 { + target_dir.pop(); + } + + let filename = src.file_name().expect("library path has no file name"); + let dst = target_dir.join(filename); + + // Best-effort. Some CI / read-only filesystems can't write here; the + // build still succeeds if so, and the rpath will fail at runtime with + // a clearer message than a mysterious copy failure. + if let Err(e) = fs::copy(src, &dst) { + println!( + "cargo:warning=mpt-crypto-sys: could not copy {} to {}: {e}", + src.display(), + dst.display() + ); + } + + // Mirror next to the examples directory on Windows so `cargo run + // --example ` finds the DLL without mucking with %PATH%. + if is_windows { + let examples_dir = target_dir.join("examples"); + if examples_dir.exists() { + let _ = fs::copy(src, examples_dir.join(filename)); + } + } +} diff --git a/internal/mpt-crypto-sys/scripts/fetch_upstream.sh b/internal/mpt-crypto-sys/scripts/fetch_upstream.sh new file mode 100755 index 00000000..996adeae --- /dev/null +++ b/internal/mpt-crypto-sys/scripts/fetch_upstream.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Refresh vendored mpt-crypto binaries from an upstream GitHub release. +# +# Run when bumping MPT_CRYPTO_VERSION in build.rs. After this script, also: +# 1. Update MPT_CRYPTO_VERSION in build.rs to the new tag. +# 2. Update BUNDLE_SHA256 in build.rs to the value this script prints. +# 3. scripts/regenerate_bindings.sh — refresh src/bindings.rs. +# 4. Commit: vendor/include, vendor/lib, src/bindings.rs, build.rs. +# +# Requires: curl, tar, shasum. +# +# Usage: +# scripts/fetch_upstream.sh +# scripts/fetch_upstream.sh 0.3.0-rc2 + +set -euo pipefail + +TAG="${1:-}" +if [ -z "$TAG" ]; then + echo "Usage: $0 (e.g. 0.3.0-rc2)" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CRATE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +VENDOR="$CRATE_ROOT/vendor" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +URL="https://github.com/XRPLF/mpt-crypto/releases/download/${TAG}/mpt-crypto-natives-${TAG}.tar.gz" +BUNDLE="$TMP/bundle.tar.gz" + +echo "==> Downloading ${URL}" +curl -sSL -o "$BUNDLE" "$URL" + +echo "==> Verifying download" +BUNDLE_SHA256="$(shasum -a 256 "$BUNDLE" | awk '{print $1}')" +echo " SHA-256: $BUNDLE_SHA256" + +echo "==> Extracting to $VENDOR/" +mkdir -p "$VENDOR/include/utility" "$VENDOR/lib" +tar -xzf "$BUNDLE" -C "$TMP" + +cp "$TMP/include/secp256k1_mpt.h" "$VENDOR/include/" +cp "$TMP/include/utility/mpt_utility.h" "$VENDOR/include/utility/" + +# Map upstream platform names → Rust target triples +for PAIR in \ + "darwin-aarch64:aarch64-apple-darwin" \ + "darwin-x86-64:x86_64-apple-darwin" \ + "linux-aarch64:aarch64-unknown-linux-gnu" \ + "linux-x86-64:x86_64-unknown-linux-gnu" \ + "linux-s390x:s390x-unknown-linux-gnu" \ + "win32-x86-64:x86_64-pc-windows-msvc" +do + UP="${PAIR%%:*}" + TARGET="${PAIR##*:}" + if [ -d "$TMP/$UP" ]; then + mkdir -p "$VENDOR/lib/$TARGET" + cp "$TMP/$UP/"* "$VENDOR/lib/$TARGET/" + echo " $UP → $TARGET" + else + echo " $UP missing in bundle (skipping)" + fi +done + +echo "" +echo "==> Done." +echo "" +echo "Next: update build.rs constants and run scripts/regenerate_bindings.sh" +echo " MPT_CRYPTO_VERSION = \"$TAG\"" +echo " BUNDLE_SHA256 = \"$BUNDLE_SHA256\"" diff --git a/internal/mpt-crypto-sys/scripts/regenerate_bindings.sh b/internal/mpt-crypto-sys/scripts/regenerate_bindings.sh new file mode 100755 index 00000000..f562cdf8 --- /dev/null +++ b/internal/mpt-crypto-sys/scripts/regenerate_bindings.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Regenerate src/bindings.rs from the vendored C headers. +# +# Requires bindgen-cli and libclang. Install bindgen-cli with: +# cargo install bindgen-cli +# +# Downstream consumers of this crate do NOT need bindgen or libclang — +# `src/bindings.rs` is committed. Only maintainers run this script. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CRATE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +if ! command -v bindgen &> /dev/null; then + echo "ERROR: bindgen CLI not found. Install with:" >&2 + echo " cargo install bindgen-cli" >&2 + exit 1 +fi + +INCLUDE="$CRATE_ROOT/vendor/include" +OUT="$CRATE_ROOT/src/bindings.rs" + +echo "==> Regenerating $OUT" + +# Use a single-header shim (placed in vendor/include/ so clang resolves +# relative includes correctly) that #includes both public headers. +# bindgen parses this in one pass, producing bindings for both the +# primitive layer (secp256k1_mpt.h) and the utility layer (mpt_utility.h). +SHIM="$INCLUDE/_shim_all.h" +cat > "$SHIM" <<'EOF' +/* Autogenerated shim for bindgen. Do not commit. */ +#include "secp256k1_mpt.h" +#include "utility/mpt_utility.h" +EOF +trap 'rm -f "$SHIM"' EXIT + +bindgen \ + --allowlist-function 'secp256k1_.*|mpt_.*|generate_canonical_.*' \ + --allowlist-type 'mpt_.*|secp256k1_.*|account_id' \ + --allowlist-var 'SECP256K1_.*|kMPT_.*|tt[A-Z].*' \ + --output "$OUT" \ + "$SHIM" \ + -- -I"$INCLUDE" + +# Prepend a generated-file warning header. +HEADER="// This file was generated by scripts/regenerate_bindings.sh. +// DO NOT EDIT BY HAND. Run that script to refresh after updating vendor/include. +// Upstream source: https://github.com/XRPLF/mpt-crypto +// +" +printf "%s%s" "$HEADER" "$(cat "$OUT")" > "$OUT" + +echo "==> Done. $(wc -l < "$OUT") lines." diff --git a/internal/mpt-crypto-sys/src/bindings.rs b/internal/mpt-crypto-sys/src/bindings.rs new file mode 100644 index 00000000..d3fa83b3 --- /dev/null +++ b/internal/mpt-crypto-sys/src/bindings.rs @@ -0,0 +1,998 @@ +// This file was generated by scripts/regenerate_bindings.sh. +// DO NOT EDIT BY HAND. Run that script to refresh after updating vendor/include. +// Upstream source: https://github.com/XRPLF/mpt-crypto +// +/* automatically generated by rust-bindgen 0.72.1 */ + +pub const SECP256K1_FLAGS_TYPE_MASK: u32 = 255; +pub const SECP256K1_FLAGS_TYPE_CONTEXT: u32 = 1; +pub const SECP256K1_FLAGS_TYPE_COMPRESSION: u32 = 2; +pub const SECP256K1_FLAGS_BIT_CONTEXT_VERIFY: u32 = 256; +pub const SECP256K1_FLAGS_BIT_CONTEXT_SIGN: u32 = 512; +pub const SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY: u32 = 1024; +pub const SECP256K1_FLAGS_BIT_COMPRESSION: u32 = 256; +pub const SECP256K1_CONTEXT_NONE: u32 = 1; +pub const SECP256K1_CONTEXT_VERIFY: u32 = 257; +pub const SECP256K1_CONTEXT_SIGN: u32 = 513; +pub const SECP256K1_CONTEXT_DECLASSIFY: u32 = 1025; +pub const SECP256K1_EC_COMPRESSED: u32 = 258; +pub const SECP256K1_EC_UNCOMPRESSED: u32 = 2; +pub const SECP256K1_TAG_PUBKEY_EVEN: u32 = 2; +pub const SECP256K1_TAG_PUBKEY_ODD: u32 = 3; +pub const SECP256K1_TAG_PUBKEY_UNCOMPRESSED: u32 = 4; +pub const SECP256K1_TAG_PUBKEY_HYBRID_EVEN: u32 = 6; +pub const SECP256K1_TAG_PUBKEY_HYBRID_ODD: u32 = 7; +pub const SECP256K1_POK_SK_PROOF_SIZE: u32 = 64; +pub const SECP256K1_COMPACT_STANDARD_PROOF_SIZE: u32 = 192; +pub const SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE: u32 = 64; +pub const SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE: u32 = 128; +pub const ttCONFIDENTIAL_MPT_CONVERT: u32 = 85; +pub const ttCONFIDENTIAL_MPT_MERGE_INBOX: u32 = 86; +pub const ttCONFIDENTIAL_MPT_CONVERT_BACK: u32 = 87; +pub const ttCONFIDENTIAL_MPT_SEND: u32 = 88; +pub const ttCONFIDENTIAL_MPT_CLAWBACK: u32 = 89; +pub const kMPT_HALF_SHA_SIZE: u32 = 32; +pub const kMPT_PUBKEY_SIZE: u32 = 33; +pub const kMPT_PRIVKEY_SIZE: u32 = 32; +pub const kMPT_BLINDING_FACTOR_SIZE: u32 = 32; +pub const kMPT_ELGAMAL_CIPHER_SIZE: u32 = 33; +pub const kMPT_ELGAMAL_TOTAL_SIZE: u32 = 66; +pub const kMPT_PEDERSEN_COMMIT_SIZE: u32 = 33; +pub const kMPT_SCHNORR_PROOF_SIZE: u32 = 64; +pub const kMPT_SINGLE_BULLETPROOF_SIZE: u32 = 688; +pub const kMPT_DOUBLE_BULLETPROOF_SIZE: u32 = 754; +pub const kMPT_ZKP_CONTEXT_HASH_SIZE: u32 = 74; +pub const kMPT_ACCOUNT_ID_SIZE: u32 = 20; +pub const kMPT_ISSUANCE_ID_SIZE: u32 = 24; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct secp256k1_context_struct { + _unused: [u8; 0], +} +#[doc = " Opaque data structure that holds context information\n\n The primary purpose of context objects is to store randomization data for\n enhanced protection against side-channel leakage. This protection is only\n effective if the context is randomized after its creation. See\n secp256k1_context_create for creation of contexts and\n secp256k1_context_randomize for randomization.\n\n A secondary purpose of context objects is to store pointers to callback\n functions that the library will call when certain error states arise. See\n secp256k1_context_set_error_callback as well as\n secp256k1_context_set_illegal_callback for details. Future library versions\n may use context objects for additional purposes.\n\n A constructed context can safely be used from multiple threads\n simultaneously, but API calls that take a non-const pointer to a context\n need exclusive access to it. In particular this is the case for\n secp256k1_context_destroy, secp256k1_context_preallocated_destroy,\n and secp256k1_context_randomize.\n\n Regarding randomization, either do it once at creation time (in which case\n you do not need any locking for the other calls), or use a read-write lock."] +pub type secp256k1_context = secp256k1_context_struct; +#[doc = " Opaque data structure that holds a parsed and valid public key.\n\n The exact representation of data inside is implementation defined and not\n guaranteed to be portable between different platforms or versions. It is\n however guaranteed to be 64 bytes in size, and can be safely copied/moved.\n If you need to convert to a format suitable for storage or transmission,\n use secp256k1_ec_pubkey_serialize and secp256k1_ec_pubkey_parse. To\n compare keys, use secp256k1_ec_pubkey_cmp."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct secp256k1_pubkey { + pub data: [::std::os::raw::c_uchar; 64usize], +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of secp256k1_pubkey"][::std::mem::size_of::() - 64usize]; + ["Alignment of secp256k1_pubkey"][::std::mem::align_of::() - 1usize]; + ["Offset of field: secp256k1_pubkey::data"] + [::std::mem::offset_of!(secp256k1_pubkey, data) - 0usize]; +}; +#[doc = " Opaque data structure that holds a parsed ECDSA signature.\n\n The exact representation of data inside is implementation defined and not\n guaranteed to be portable between different platforms or versions. It is\n however guaranteed to be 64 bytes in size, and can be safely copied/moved.\n If you need to convert to a format suitable for storage, transmission, or\n comparison, use the secp256k1_ecdsa_signature_serialize_* and\n secp256k1_ecdsa_signature_parse_* functions."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct secp256k1_ecdsa_signature { + pub data: [::std::os::raw::c_uchar; 64usize], +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of secp256k1_ecdsa_signature"] + [::std::mem::size_of::() - 64usize]; + ["Alignment of secp256k1_ecdsa_signature"] + [::std::mem::align_of::() - 1usize]; + ["Offset of field: secp256k1_ecdsa_signature::data"] + [::std::mem::offset_of!(secp256k1_ecdsa_signature, data) - 0usize]; +}; +#[doc = " A pointer to a function to deterministically generate a nonce.\n\n Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail.\n Out: nonce32: pointer to a 32-byte array to be filled by the function.\n In: msg32: the 32-byte message hash being verified (will not be NULL)\n key32: pointer to a 32-byte secret key (will not be NULL)\n algo16: pointer to a 16-byte array describing the signature\n algorithm (will be NULL for ECDSA for compatibility).\n data: Arbitrary data pointer that is passed through.\n attempt: how many iterations we have tried to find a nonce.\n This will almost always be 0, but different attempt values\n are required to result in a different nonce.\n\n Except for test cases, this function should compute some cryptographic hash of\n the message, the algorithm, the key and the attempt."] +pub type secp256k1_nonce_function = ::std::option::Option< + unsafe extern "C" fn( + nonce32: *mut ::std::os::raw::c_uchar, + msg32: *const ::std::os::raw::c_uchar, + key32: *const ::std::os::raw::c_uchar, + algo16: *const ::std::os::raw::c_uchar, + data: *mut ::std::os::raw::c_void, + attempt: ::std::os::raw::c_uint, + ) -> ::std::os::raw::c_int, +>; +unsafe extern "C" { + #[doc = " Perform basic self tests (to be used in conjunction with secp256k1_context_static)\n\n This function performs self tests that detect some serious usage errors and\n similar conditions, e.g., when the library is compiled for the wrong endianness.\n This is a last resort measure to be used in production. The performed tests are\n very rudimentary and are not intended as a replacement for running the test\n binaries.\n\n It is highly recommended to call this before using secp256k1_context_static.\n It is not necessary to call this function before using a context created with\n secp256k1_context_create (or secp256k1_context_preallocated_create), which will\n take care of performing the self tests.\n\n If the tests fail, this function will call the default error callback to abort the\n program (see secp256k1_context_set_error_callback)."] + pub fn secp256k1_selftest(); +} +unsafe extern "C" { + #[doc = " Create a secp256k1 context object (in dynamically allocated memory).\n\n This function uses malloc to allocate memory. It is guaranteed that malloc is\n called at most once for every call of this function. If you need to avoid dynamic\n memory allocation entirely, see secp256k1_context_static and the functions in\n secp256k1_preallocated.h.\n\n Returns: pointer to a newly created context object.\n In: flags: Always set to SECP256K1_CONTEXT_NONE (see below).\n\n The only valid non-deprecated flag in recent library versions is\n SECP256K1_CONTEXT_NONE, which will create a context sufficient for all functionality\n offered by the library. All other (deprecated) flags will be treated as equivalent\n to the SECP256K1_CONTEXT_NONE flag. Though the flags parameter primarily exists for\n historical reasons, future versions of the library may introduce new flags.\n\n If the context is intended to be used for API functions that perform computations\n involving secret keys, e.g., signing and public key generation, then it is highly\n recommended to call secp256k1_context_randomize on the context before calling\n those API functions. This will provide enhanced protection against side-channel\n leakage, see secp256k1_context_randomize for details.\n\n Do not create a new context object for each operation, as construction and\n randomization can take non-negligible time."] + pub fn secp256k1_context_create(flags: ::std::os::raw::c_uint) -> *mut secp256k1_context; +} +unsafe extern "C" { + #[doc = " Copy a secp256k1 context object (into dynamically allocated memory).\n\n This function uses malloc to allocate memory. It is guaranteed that malloc is\n called at most once for every call of this function. If you need to avoid dynamic\n memory allocation entirely, see the functions in secp256k1_preallocated.h.\n\n Cloning secp256k1_context_static is not possible, and should not be emulated by\n the caller (e.g., using memcpy). Create a new context instead.\n\n Returns: pointer to a newly created context object.\n Args: ctx: pointer to a context to copy (not secp256k1_context_static)."] + pub fn secp256k1_context_clone(ctx: *const secp256k1_context) -> *mut secp256k1_context; +} +unsafe extern "C" { + #[doc = " Destroy a secp256k1 context object (created in dynamically allocated memory).\n\n The context pointer may not be used afterwards.\n\n The context to destroy must have been created using secp256k1_context_create\n or secp256k1_context_clone. If the context has instead been created using\n secp256k1_context_preallocated_create or secp256k1_context_preallocated_clone, the\n behaviour is undefined. In that case, secp256k1_context_preallocated_destroy must\n be used instead.\n\n Args: ctx: pointer to a context to destroy, constructed using\n secp256k1_context_create or secp256k1_context_clone\n (i.e., not secp256k1_context_static)."] + pub fn secp256k1_context_destroy(ctx: *mut secp256k1_context); +} +unsafe extern "C" { + #[doc = " Set a callback function to be called when an illegal argument is passed to\n an API call. It will only trigger for violations that are mentioned\n explicitly in the header.\n\n The philosophy is that these shouldn't be dealt with through a specific\n return value, as calling code should not have branches to deal with the case\n that this code itself is broken.\n\n On the other hand, during debug stage, one would want to be informed about\n such mistakes, and the default (crashing) may be inadvisable. Should this\n callback return instead of crashing, the return value and output arguments\n of the API function call are undefined. Moreover, the same API call may\n trigger the callback again in this case.\n\n When this function has not been called (or called with fun==NULL), then the\n default callback will be used. The library provides a default callback which\n writes the message to stderr and calls abort. This default callback can be\n replaced at link time if the preprocessor macro\n USE_EXTERNAL_DEFAULT_CALLBACKS is defined, which is the case if the build\n has been configured with --enable-external-default-callbacks (GNU Autotools) or\n -DSECP256K1_USE_EXTERNAL_DEFAULT_CALLBACKS=ON (CMake). Then the\n following two symbols must be provided to link against:\n - void secp256k1_default_illegal_callback_fn(const char *message, void *data);\n - void secp256k1_default_error_callback_fn(const char *message, void *data);\n The library may call a default callback even before a proper callback data\n pointer could have been set using secp256k1_context_set_illegal_callback or\n secp256k1_context_set_error_callback, e.g., when the creation of a context\n fails. In this case, the corresponding default callback will be called with\n the data pointer argument set to NULL.\n\n Args: ctx: pointer to a context object.\n In: fun: pointer to a function to call when an illegal argument is\n passed to the API, taking a message and an opaque pointer.\n (NULL restores the default callback.)\n data: the opaque pointer to pass to fun above, must be NULL for the\n default callback.\n\n See also secp256k1_context_set_error_callback."] + pub fn secp256k1_context_set_illegal_callback( + ctx: *mut secp256k1_context, + fun: ::std::option::Option< + unsafe extern "C" fn( + message: *const ::std::os::raw::c_char, + data: *mut ::std::os::raw::c_void, + ), + >, + data: *const ::std::os::raw::c_void, + ); +} +unsafe extern "C" { + #[doc = " Set a callback function to be called when an internal consistency check\n fails.\n\n The default callback writes an error message to stderr and calls abort\n to abort the program.\n\n This can only trigger in case of a hardware failure, miscompilation,\n memory corruption, serious bug in the library, or other error that would\n result in undefined behaviour. It will not trigger due to mere\n incorrect usage of the API (see secp256k1_context_set_illegal_callback\n for that). After this callback returns, anything may happen, including\n crashing.\n\n Args: ctx: pointer to a context object.\n In: fun: pointer to a function to call when an internal error occurs,\n taking a message and an opaque pointer (NULL restores the\n default callback, see secp256k1_context_set_illegal_callback\n for details).\n data: the opaque pointer to pass to fun above, must be NULL for the\n default callback.\n\n See also secp256k1_context_set_illegal_callback."] + pub fn secp256k1_context_set_error_callback( + ctx: *mut secp256k1_context, + fun: ::std::option::Option< + unsafe extern "C" fn( + message: *const ::std::os::raw::c_char, + data: *mut ::std::os::raw::c_void, + ), + >, + data: *const ::std::os::raw::c_void, + ); +} +#[doc = " A pointer to a function implementing SHA256's internal compression function.\n\n This function processes one or more contiguous 64-byte message blocks and\n updates the internal SHA256 state accordingly. The function is not responsible\n for counting consumed blocks or bytes, nor for performing padding.\n\n In/Out: state: pointer to eight 32-bit words representing the current internal state;\n the state is updated in place.\n In: blocks64: pointer to concatenation of n_blocks blocks, of 64 bytes each.\n no alignment guarantees are made for this pointer.\n n_blocks: number of contiguous 64-byte blocks to process."] +pub type secp256k1_sha256_compression_function = ::std::option::Option< + unsafe extern "C" fn( + state: *mut u32, + blocks64: *const ::std::os::raw::c_uchar, + n_blocks: usize, + ), +>; +unsafe extern "C" { + #[doc = " Set a callback function to override the internal SHA256 compression function.\n\n This installs a function to replace the built-in block-compression\n step used by the library's internal SHA256 implementation.\n The provided callback must exactly implement the effect of n_blocks\n repeated applications of the SHA256 compression function.\n\n This API exists to support environments that wish to route the\n SHA256 compression step through a hardware-accelerated or otherwise\n specialized implementation. It is NOT meant for replacing SHA256\n with a different hash function.\n\n Args: ctx: pointer to a context object.\n In: fn_compression: pointer to a function implementing the compression function;\n passing NULL restores the default implementation."] + pub fn secp256k1_context_set_sha256_compression( + ctx: *mut secp256k1_context, + fn_compression: secp256k1_sha256_compression_function, + ); +} +unsafe extern "C" { + #[doc = " Parse a variable-length public key into the pubkey object.\n\n Returns: 1 if the public key was fully valid.\n 0 if the public key could not be parsed or is invalid.\n Args: ctx: pointer to a context object.\n Out: pubkey: pointer to a pubkey object. If 1 is returned, it is set to a\n parsed version of input. If not, its value is undefined.\n In: input: pointer to a serialized public key\n inputlen: length of the array pointed to by input\n\n This function supports parsing compressed (33 bytes, header byte 0x02 or\n 0x03), uncompressed (65 bytes, header byte 0x04), or hybrid (65 bytes, header\n byte 0x06 or 0x07) format public keys."] + pub fn secp256k1_ec_pubkey_parse( + ctx: *const secp256k1_context, + pubkey: *mut secp256k1_pubkey, + input: *const ::std::os::raw::c_uchar, + inputlen: usize, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Serialize a pubkey object into a serialized byte sequence.\n\n Returns: 1 always.\n Args: ctx: pointer to a context object.\n Out: output: pointer to a 65-byte (if compressed==0) or 33-byte (if\n compressed==1) byte array to place the serialized key\n in.\n In/Out: outputlen: pointer to an integer which is initially set to the\n size of output, and is overwritten with the written\n size.\n In: pubkey: pointer to a secp256k1_pubkey containing an\n initialized public key.\n flags: SECP256K1_EC_COMPRESSED if serialization should be in\n compressed format, otherwise SECP256K1_EC_UNCOMPRESSED."] + pub fn secp256k1_ec_pubkey_serialize( + ctx: *const secp256k1_context, + output: *mut ::std::os::raw::c_uchar, + outputlen: *mut usize, + pubkey: *const secp256k1_pubkey, + flags: ::std::os::raw::c_uint, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Compare two public keys using lexicographic (of compressed serialization) order\n\n Returns: <0 if the first public key is less than the second\n >0 if the first public key is greater than the second\n 0 if the two public keys are equal\n Args: ctx: pointer to a context object\n In: pubkey1: first public key to compare\n pubkey2: second public key to compare"] + pub fn secp256k1_ec_pubkey_cmp( + ctx: *const secp256k1_context, + pubkey1: *const secp256k1_pubkey, + pubkey2: *const secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Sort public keys using lexicographic (of compressed serialization) order\n\n Returns: 0 if the arguments are invalid. 1 otherwise.\n\n Args: ctx: pointer to a context object\n In: pubkeys: array of pointers to pubkeys to sort\n n_pubkeys: number of elements in the pubkeys array"] + pub fn secp256k1_ec_pubkey_sort( + ctx: *const secp256k1_context, + pubkeys: *mut *const secp256k1_pubkey, + n_pubkeys: usize, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Parse an ECDSA signature in compact (64 bytes) format.\n\n Returns: 1 when the signature could be parsed, 0 otherwise.\n Args: ctx: pointer to a context object\n Out: sig: pointer to a signature object\n In: input64: pointer to the 64-byte array to parse\n\n The signature must consist of a 32-byte big endian R value, followed by a\n 32-byte big endian S value. If R or S fall outside of [0..order-1], the\n encoding is invalid. R and S with value 0 are allowed in the encoding.\n\n After the call, sig will always be initialized. If parsing failed or R or\n S are zero, the resulting sig value is guaranteed to fail verification for\n any message and public key."] + pub fn secp256k1_ecdsa_signature_parse_compact( + ctx: *const secp256k1_context, + sig: *mut secp256k1_ecdsa_signature, + input64: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Parse a DER ECDSA signature.\n\n Returns: 1 when the signature could be parsed, 0 otherwise.\n Args: ctx: pointer to a context object\n Out: sig: pointer to a signature object\n In: input: pointer to the signature to be parsed\n inputlen: the length of the array pointed to be input\n\n This function will accept any valid DER encoded signature, even if the\n encoded numbers are out of range.\n\n After the call, sig will always be initialized. If parsing failed or the\n encoded numbers are out of range, signature verification with it is\n guaranteed to fail for every message and public key."] + pub fn secp256k1_ecdsa_signature_parse_der( + ctx: *const secp256k1_context, + sig: *mut secp256k1_ecdsa_signature, + input: *const ::std::os::raw::c_uchar, + inputlen: usize, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Serialize an ECDSA signature in DER format.\n\n Returns: 1 if enough space was available to serialize, 0 otherwise\n Args: ctx: pointer to a context object\n Out: output: pointer to an array to store the DER serialization\n In/Out: outputlen: pointer to a length integer. Initially, this integer\n should be set to the length of output. After the call\n it will be set to the length of the serialization (even\n if 0 was returned).\n In: sig: pointer to an initialized signature object"] + pub fn secp256k1_ecdsa_signature_serialize_der( + ctx: *const secp256k1_context, + output: *mut ::std::os::raw::c_uchar, + outputlen: *mut usize, + sig: *const secp256k1_ecdsa_signature, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Serialize an ECDSA signature in compact (64 byte) format.\n\n Returns: 1\n Args: ctx: pointer to a context object\n Out: output64: pointer to a 64-byte array to store the compact serialization\n In: sig: pointer to an initialized signature object\n\n See secp256k1_ecdsa_signature_parse_compact for details about the encoding."] + pub fn secp256k1_ecdsa_signature_serialize_compact( + ctx: *const secp256k1_context, + output64: *mut ::std::os::raw::c_uchar, + sig: *const secp256k1_ecdsa_signature, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Verify an ECDSA signature.\n\n Returns: 1: correct signature\n 0: incorrect or unparseable signature\n Args: ctx: pointer to a context object\n In: sig: the signature being verified.\n msghash32: the 32-byte message hash being verified.\n The verifier must make sure to apply a cryptographic\n hash function to the message by itself and not accept an\n msghash32 value directly. Otherwise, it would be easy to\n create a \"valid\" signature without knowledge of the\n secret key. See also\n https://bitcoin.stackexchange.com/a/81116/35586 for more\n background on this topic.\n pubkey: pointer to an initialized public key to verify with.\n\n To avoid accepting malleable signatures, only ECDSA signatures in lower-S\n form are accepted.\n\n If you need to accept ECDSA signatures from sources that do not obey this\n rule, apply secp256k1_ecdsa_signature_normalize to the signature prior to\n verification, but be aware that doing so results in malleable signatures.\n\n For details, see the comments for that function."] + pub fn secp256k1_ecdsa_verify( + ctx: *const secp256k1_context, + sig: *const secp256k1_ecdsa_signature, + msghash32: *const ::std::os::raw::c_uchar, + pubkey: *const secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Convert a signature to a normalized lower-S form.\n\n Returns: 1 if sigin was not normalized, 0 if it already was.\n Args: ctx: pointer to a context object\n Out: sigout: pointer to a signature to fill with the normalized form,\n or copy if the input was already normalized. (can be NULL if\n you're only interested in whether the input was already\n normalized).\n In: sigin: pointer to a signature to check/normalize (can be identical to sigout)\n\n With ECDSA a third-party can forge a second distinct signature of the same\n message, given a single initial signature, but without knowing the key. This\n is done by negating the S value modulo the order of the curve, 'flipping'\n the sign of the random point R which is not included in the signature.\n\n Forgery of the same message isn't universally problematic, but in systems\n where message malleability or uniqueness of signatures is important this can\n cause issues. This forgery can be blocked by all verifiers forcing signers\n to use a normalized form.\n\n The lower-S form reduces the size of signatures slightly on average when\n variable length encodings (such as DER) are used and is cheap to verify,\n making it a good choice. Security of always using lower-S is assured because\n anyone can trivially modify a signature after the fact to enforce this\n property anyway.\n\n The lower S value is always between 0x1 and\n 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0,\n inclusive.\n\n No other forms of ECDSA malleability are known and none seem likely, but\n there is no formal proof that ECDSA, even with this additional restriction,\n is free of other malleability. Commonly used serialization schemes will also\n accept various non-unique encodings, so care should be taken when this\n property is required for an application.\n\n The secp256k1_ecdsa_sign function will by default create signatures in the\n lower-S form, and secp256k1_ecdsa_verify will not accept others. In case\n signatures come from a system that cannot enforce this property,\n secp256k1_ecdsa_signature_normalize must be called before verification."] + pub fn secp256k1_ecdsa_signature_normalize( + ctx: *const secp256k1_context, + sigout: *mut secp256k1_ecdsa_signature, + sigin: *const secp256k1_ecdsa_signature, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Create an ECDSA signature.\n\n Returns: 1: signature created\n 0: the nonce generation function failed, or the secret key was invalid.\n Args: ctx: pointer to a context object (not secp256k1_context_static).\n Out: sig: pointer to an array where the signature will be placed.\n In: msghash32: the 32-byte message hash being signed.\n seckey: pointer to a 32-byte secret key.\n noncefp: pointer to a nonce generation function. If NULL,\n secp256k1_nonce_function_default is used.\n ndata: pointer to arbitrary data used by the nonce generation function\n (can be NULL). If it is non-NULL and\n secp256k1_nonce_function_default is used, then ndata must be a\n pointer to 32-bytes of additional data.\n\n The created signature is always in lower-S form. See\n secp256k1_ecdsa_signature_normalize for more details."] + pub fn secp256k1_ecdsa_sign( + ctx: *const secp256k1_context, + sig: *mut secp256k1_ecdsa_signature, + msghash32: *const ::std::os::raw::c_uchar, + seckey: *const ::std::os::raw::c_uchar, + noncefp: secp256k1_nonce_function, + ndata: *const ::std::os::raw::c_void, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Verify an elliptic curve secret key.\n\n A secret key is valid if it is not 0 and less than the secp256k1 curve order\n when interpreted as an integer (most significant byte first). The\n probability of choosing a 32-byte string uniformly at random which is an\n invalid secret key is negligible. However, if it does happen it should\n be assumed that the randomness source is severely broken and there should\n be no retry.\n\n Returns: 1: secret key is valid\n 0: secret key is invalid\n Args: ctx: pointer to a context object.\n In: seckey: pointer to a 32-byte secret key."] + pub fn secp256k1_ec_seckey_verify( + ctx: *const secp256k1_context, + seckey: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Compute the public key for a secret key.\n\n Returns: 1: secret was valid, public key stores.\n 0: secret was invalid, try again.\n Args: ctx: pointer to a context object (not secp256k1_context_static).\n Out: pubkey: pointer to the created public key.\n In: seckey: pointer to a 32-byte secret key."] + pub fn secp256k1_ec_pubkey_create( + ctx: *const secp256k1_context, + pubkey: *mut secp256k1_pubkey, + seckey: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Negates a secret key in place.\n\n Returns: 0 if the given secret key is invalid according to\n secp256k1_ec_seckey_verify. 1 otherwise\n Args: ctx: pointer to a context object\n In/Out: seckey: pointer to the 32-byte secret key to be negated. If the\n secret key is invalid according to\n secp256k1_ec_seckey_verify, this function returns 0 and\n seckey will be set to some unspecified value."] + pub fn secp256k1_ec_seckey_negate( + ctx: *const secp256k1_context, + seckey: *mut ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Negates a public key in place.\n\n Returns: 1 always\n Args: ctx: pointer to a context object\n In/Out: pubkey: pointer to the public key to be negated."] + pub fn secp256k1_ec_pubkey_negate( + ctx: *const secp256k1_context, + pubkey: *mut secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Tweak a secret key by adding tweak to it.\n\n Returns: 0 if the arguments are invalid or the resulting secret key would be\n invalid (only when the tweak is the negation of the secret key). 1\n otherwise.\n Args: ctx: pointer to a context object.\n In/Out: seckey: pointer to a 32-byte secret key. If the secret key is\n invalid according to secp256k1_ec_seckey_verify, this\n function returns 0. seckey will be set to some unspecified\n value if this function returns 0.\n In: tweak32: pointer to a 32-byte tweak, which must be valid according to\n secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly\n random 32-byte tweaks, the chance of being invalid is\n negligible (around 1 in 2^128)."] + pub fn secp256k1_ec_seckey_tweak_add( + ctx: *const secp256k1_context, + seckey: *mut ::std::os::raw::c_uchar, + tweak32: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Tweak a public key by adding tweak times the generator to it.\n\n Returns: 0 if the arguments are invalid or the resulting public key would be\n invalid (only when the tweak is the negation of the corresponding\n secret key). 1 otherwise.\n Args: ctx: pointer to a context object.\n In/Out: pubkey: pointer to a public key object. pubkey will be set to an\n invalid value if this function returns 0.\n In: tweak32: pointer to a 32-byte tweak, which must be valid according to\n secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly\n random 32-byte tweaks, the chance of being invalid is\n negligible (around 1 in 2^128)."] + pub fn secp256k1_ec_pubkey_tweak_add( + ctx: *const secp256k1_context, + pubkey: *mut secp256k1_pubkey, + tweak32: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Tweak a secret key by multiplying it by a tweak.\n\n Returns: 0 if the arguments are invalid. 1 otherwise.\n Args: ctx: pointer to a context object.\n In/Out: seckey: pointer to a 32-byte secret key. If the secret key is\n invalid according to secp256k1_ec_seckey_verify, this\n function returns 0. seckey will be set to some unspecified\n value if this function returns 0.\n In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to\n secp256k1_ec_seckey_verify, this function returns 0. For\n uniformly random 32-byte arrays the chance of being invalid\n is negligible (around 1 in 2^128)."] + pub fn secp256k1_ec_seckey_tweak_mul( + ctx: *const secp256k1_context, + seckey: *mut ::std::os::raw::c_uchar, + tweak32: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Tweak a public key by multiplying it by a tweak value.\n\n Returns: 0 if the arguments are invalid. 1 otherwise.\n Args: ctx: pointer to a context object.\n In/Out: pubkey: pointer to a public key object. pubkey will be set to an\n invalid value if this function returns 0.\n In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to\n secp256k1_ec_seckey_verify, this function returns 0. For\n uniformly random 32-byte arrays the chance of being invalid\n is negligible (around 1 in 2^128)."] + pub fn secp256k1_ec_pubkey_tweak_mul( + ctx: *const secp256k1_context, + pubkey: *mut secp256k1_pubkey, + tweak32: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Randomizes the context to provide enhanced protection against side-channel leakage.\n\n Returns: 1: randomization successful\n 0: error\n Args: ctx: pointer to a context object (not secp256k1_context_static).\n In: seed32: pointer to a 32-byte random seed (NULL resets to initial state).\n\n While secp256k1 code is written and tested to be constant-time no matter what\n secret values are, it is possible that a compiler may output code which is not,\n and also that the CPU may not emit the same radio frequencies or draw the same\n amount of power for all values. Randomization of the context shields against\n side-channel observations which aim to exploit secret-dependent behaviour in\n certain computations which involve secret keys.\n\n It is highly recommended to call this function on contexts returned from\n secp256k1_context_create or secp256k1_context_clone (or from the corresponding\n functions in secp256k1_preallocated.h) before using these contexts to call API\n functions that perform computations involving secret keys, e.g., signing and\n public key generation. It is possible to call this function more than once on\n the same context, and doing so before every few computations involving secret\n keys is recommended as a defense-in-depth measure. Randomization of the static\n context secp256k1_context_static is not supported.\n\n Currently, the random seed is mainly used for blinding multiplications of a\n secret scalar with the elliptic curve base point. Multiplications of this\n kind are performed by exactly those API functions which are documented to\n require a context that is not secp256k1_context_static. As a rule of thumb,\n these are all functions which take a secret key (or a keypair) as an input.\n A notable exception to that rule is the ECDH module, which relies on a different\n kind of elliptic curve point multiplication and thus does not benefit from\n enhanced protection against side-channel leakage currently."] + pub fn secp256k1_context_randomize( + ctx: *mut secp256k1_context, + seed32: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Add a number of public keys together.\n\n Returns: 1: the sum of the public keys is valid.\n 0: the sum of the public keys is not valid.\n Args: ctx: pointer to a context object.\n Out: out: pointer to a public key object for placing the resulting public key.\n In: ins: pointer to array of pointers to public keys.\n n: the number of public keys to add together (must be at least 1)."] + pub fn secp256k1_ec_pubkey_combine( + ctx: *const secp256k1_context, + out: *mut secp256k1_pubkey, + ins: *const *const secp256k1_pubkey, + n: usize, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Compute a tagged hash as defined in BIP-340.\n\n This is useful for creating a message hash and achieving domain separation\n through an application-specific tag. This function returns\n SHA256(SHA256(tag)||SHA256(tag)||msg). Therefore, tagged hash\n implementations optimized for a specific tag can precompute the SHA256 state\n after hashing the tag hashes.\n\n Returns: 1 always.\n Args: ctx: pointer to a context object\n Out: hash32: pointer to a 32-byte array to store the resulting hash\n In: tag: pointer to an array containing the tag\n taglen: length of the tag array\n msg: pointer to an array containing the message\n msglen: length of the message array"] + pub fn secp256k1_tagged_sha256( + ctx: *const secp256k1_context, + hash32: *mut ::std::os::raw::c_uchar, + tag: *const ::std::os::raw::c_uchar, + taglen: usize, + msg: *const ::std::os::raw::c_uchar, + msglen: usize, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates a new secp256k1 key pair."] + pub fn secp256k1_elgamal_generate_keypair( + ctx: *const secp256k1_context, + privkey: *mut ::std::os::raw::c_uchar, + pubkey: *mut secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Encrypts a 64-bit amount using ElGamal."] + pub fn secp256k1_elgamal_encrypt( + ctx: *const secp256k1_context, + c1: *mut secp256k1_pubkey, + c2: *mut secp256k1_pubkey, + pubkey_Q: *const secp256k1_pubkey, + amount: u64, + blinding_factor: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Decrypts an ElGamal ciphertext to recover the amount."] + pub fn secp256k1_elgamal_decrypt( + ctx: *const secp256k1_context, + amount: *mut u64, + c1: *const secp256k1_pubkey, + c2: *const secp256k1_pubkey, + privkey: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Homomorphically adds two ElGamal ciphertexts."] + pub fn secp256k1_elgamal_add( + ctx: *const secp256k1_context, + sum_c1: *mut secp256k1_pubkey, + sum_c2: *mut secp256k1_pubkey, + a_c1: *const secp256k1_pubkey, + a_c2: *const secp256k1_pubkey, + b_c1: *const secp256k1_pubkey, + b_c2: *const secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Homomorphically subtracts two ElGamal ciphertexts."] + pub fn secp256k1_elgamal_subtract( + ctx: *const secp256k1_context, + diff_c1: *mut secp256k1_pubkey, + diff_c2: *mut secp256k1_pubkey, + a_c1: *const secp256k1_pubkey, + a_c2: *const secp256k1_pubkey, + b_c1: *const secp256k1_pubkey, + b_c2: *const secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates the canonical encrypted zero for a given MPT token instance.\n\n This ciphertext represents a zero balance for a specific account's holding\n of a token defined by its MPTokenIssuanceID.\n\n @param[in] ctx A pointer to a valid secp256k1 context.\n @param[out] enc_zero_c1 The C1 component of the canonical ciphertext.\n @param[out] enc_zero_c2 The C2 component of the canonical ciphertext.\n @param[in] pubkey The ElGamal public key of the account holder.\n @param[in] account_id A pointer to the 20-byte AccountID.\n @param[in] mpt_issuance_id A pointer to the 24-byte MPTokenIssuanceID.\n\n @return 1 on success, 0 on failure."] + pub fn generate_canonical_encrypted_zero( + ctx: *const secp256k1_context, + enc_zero_c1: *mut secp256k1_pubkey, + enc_zero_c2: *mut secp256k1_pubkey, + pubkey: *const secp256k1_pubkey, + account_id: *const ::std::os::raw::c_uchar, + mpt_issuance_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates a proof that an ElGamal ciphertext correctly encrypts a\n known plaintext `m` and that the prover knows the randomness `r`.\n\n @param[in] ctx A pointer to a valid secp256k1 context object,\n initialized for signing.\n @param[out] proof A pointer to a 98-byte buffer to store the proof\n (T1 [33 bytes] || T2 [33 bytes] || s [32 bytes]).\n @note Legacy uncompressed form; superseded by the compact proof APIs\n (secp256k1_compact_*). Removed in PR #22.\n @param[in] c1 The C1 component of the ciphertext (r*G).\n @param[in] c2 The C2 component of the ciphertext (m*G + r*Pk).\n @param[in] pk_recipient The public key used for encryption.\n @param[in] amount The known plaintext value `m`.\n @param[in] randomness_r The 32-byte secret random scalar `r` used in encryption.\n @param[in] tx_context_id A 32-byte unique identifier for the transaction context.\n\n @return 1 on success, 0 on failure."] + pub fn secp256k1_equality_plaintext_prove( + ctx: *const secp256k1_context, + proof: *mut ::std::os::raw::c_uchar, + c1: *const secp256k1_pubkey, + c2: *const secp256k1_pubkey, + pk_recipient: *const secp256k1_pubkey, + amount: u64, + randomness_r: *const ::std::os::raw::c_uchar, + tx_context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verifies a proof of knowledge of plaintext and randomness.\n\n Checks if the proof correctly demonstrates that (C1, C2) encrypts `m`\n under `pk_recipient`.\n\n @param[in] ctx A pointer to a valid secp256k1 context object,\n initialized for verification.\n @param[in] proof A pointer to the 98-byte proof to verify.\n @param[in] c1 The C1 component of the ciphertext.\n @param[in] c2 The C2 component of the ciphertext.\n @param[in] pk_recipient The public key used for encryption.\n @param[in] amount The known plaintext value `m`.\n @param[in] tx_context_id A 32-byte unique identifier for the transaction context.\n\n @return 1 if the proof is valid, 0 otherwise."] + pub fn secp256k1_equality_plaintext_verify( + ctx: *const secp256k1_context, + proof: *const ::std::os::raw::c_uchar, + c1: *const secp256k1_pubkey, + c2: *const secp256k1_pubkey, + pk_recipient: *const secp256k1_pubkey, + amount: u64, + tx_context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Computes a Pedersen Commitment: C = value*G + blinding_factor*Pk_base.\n\n This function creates the commitment point (C) that the Bulletproof proves\n the range of. Pk_base is the dynamic secondary generator (H).\n\n @param[in] ctx A pointer to the context.\n @param[out] commitment_C The resulting commitment point C.\n @param[in] value The secret amount v (uint64_t).\n @param[in] blinding_factor The secret randomness r (32 bytes).\n @param[in] pk_base The recipient's public key (used as the H generator).\n\n @return 1 on success, 0 on failure."] + pub fn secp256k1_bulletproof_create_commitment( + ctx: *const secp256k1_context, + commitment_C: *mut secp256k1_pubkey, + value: u64, + blinding_factor: *const ::std::os::raw::c_uchar, + pk_base: *const secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_bulletproof_prove( + ctx: *const secp256k1_context, + proof_out: *mut ::std::os::raw::c_uchar, + proof_len: *mut usize, + value: u64, + blinding_factor: *const ::std::os::raw::c_uchar, + pk_base: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + proof_type: ::std::os::raw::c_uint, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_bulletproof_verify( + ctx: *const secp256k1_context, + G_vec: *const secp256k1_pubkey, + H_vec: *const secp256k1_pubkey, + proof: *const ::std::os::raw::c_uchar, + proof_len: usize, + commitment_C: *const secp256k1_pubkey, + pk_base: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Proves the link between an ElGamal ciphertext and a Pedersen commitment.\n * Formal Statement: Knowledge of (m, r, rho) such that:\n C1 = r*G, C2 = m*G + r*Pk, and PCm = m*G + rho*H.\n * @param ctx Pointer to a secp256k1 context object.\n @param proof [OUT] Pointer to 195-byte buffer for the proof output.\n Legacy Variant B format; superseded by compact proof APIs.\n Removed in PR #22.\n @param c1 Pointer to the ElGamal C1 point (r*G).\n @param c2 Pointer to the ElGamal C2 point (m*G + r*Pk).\n @param pk Pointer to the recipient's public key.\n @param pcm Pointer to the Pedersen Commitment (m*G + rho*H).\n @param amount The plaintext amount (m).\n @param r The 32-byte secret ElGamal blinding factor.\n @param rho The 32-byte secret Pedersen blinding factor.\n @param context_id 32-byte unique transaction context identifier.\n @return 1 on success, 0 on failure."] + pub fn secp256k1_elgamal_pedersen_link_prove( + ctx: *const secp256k1_context, + proof: *mut ::std::os::raw::c_uchar, + c1: *const secp256k1_pubkey, + c2: *const secp256k1_pubkey, + pk: *const secp256k1_pubkey, + pcm: *const secp256k1_pubkey, + amount: u64, + r: *const ::std::os::raw::c_uchar, + rho: *const ::std::os::raw::c_uchar, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verifies the link proof between ElGamal and Pedersen commitments.\n * @return 1 if the proof is valid, 0 otherwise."] + pub fn secp256k1_elgamal_pedersen_link_verify( + ctx: *const secp256k1_context, + proof: *const ::std::os::raw::c_uchar, + c1: *const secp256k1_pubkey, + c2: *const secp256k1_pubkey, + pk: *const secp256k1_pubkey, + pcm: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Verifies that (c1, c2) is a valid ElGamal encryption of 'amount'\n for 'pubkey_Q' using the revealed 'blinding_factor'."] + pub fn secp256k1_elgamal_verify_encryption( + ctx: *const secp256k1_context, + c1: *const secp256k1_pubkey, + c2: *const secp256k1_pubkey, + pubkey_Q: *const secp256k1_pubkey, + amount: u64, + blinding_factor: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_mpt_pok_sk_prove( + ctx: *const secp256k1_context, + proof: *mut ::std::os::raw::c_uchar, + pk: *const secp256k1_pubkey, + sk: *const ::std::os::raw::c_uchar, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_mpt_pok_sk_verify( + ctx: *const secp256k1_context, + proof: *const ::std::os::raw::c_uchar, + pk: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Compute a Pedersen Commitment: PC = m*G + rho*H\n Returns 1 on success, 0 on failure."] + pub fn secp256k1_mpt_pedersen_commit( + ctx: *const secp256k1_context, + commitment: *mut secp256k1_pubkey, + amount: u64, + blinding_factor_rho: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Get the standardized H generator for Pedersen Commitments"] + pub fn secp256k1_mpt_get_h_generator( + ctx: *const secp256k1_context, + h: *mut secp256k1_pubkey, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates a vector of N independent NUMS generators."] + pub fn secp256k1_mpt_get_generator_vector( + ctx: *const secp256k1_context, + vec: *mut secp256k1_pubkey, + n: usize, + label: *const ::std::os::raw::c_uchar, + label_len: usize, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_mpt_scalar_add( + res: *mut ::std::os::raw::c_uchar, + a: *const ::std::os::raw::c_uchar, + b: *const ::std::os::raw::c_uchar, + ); +} +unsafe extern "C" { + pub fn secp256k1_mpt_scalar_mul( + res: *mut ::std::os::raw::c_uchar, + a: *const ::std::os::raw::c_uchar, + b: *const ::std::os::raw::c_uchar, + ); +} +unsafe extern "C" { + pub fn secp256k1_mpt_scalar_inverse( + res: *mut ::std::os::raw::c_uchar, + in_: *const ::std::os::raw::c_uchar, + ); +} +unsafe extern "C" { + pub fn secp256k1_mpt_scalar_negate( + res: *mut ::std::os::raw::c_uchar, + in_: *const ::std::os::raw::c_uchar, + ); +} +unsafe extern "C" { + pub fn secp256k1_mpt_scalar_reduce32( + out32: *mut ::std::os::raw::c_uchar, + in32: *const ::std::os::raw::c_uchar, + ); +} +unsafe extern "C" { + #[doc = " Returns the size of the serialized proof for N recipients.\n Size: (1 + N) * 33 bytes for points + 2 * 32 bytes for scalars."] + pub fn secp256k1_mpt_proof_equality_shared_r_size(n: usize) -> usize; +} +unsafe extern "C" { + #[doc = " Generates a proof that multiple ciphertexts encrypt the same amount m\n using the SAME shared randomness r."] + pub fn secp256k1_mpt_prove_equality_shared_r( + ctx: *const secp256k1_context, + proof_out: *mut ::std::os::raw::c_uchar, + amount: u64, + r_shared: *const ::std::os::raw::c_uchar, + n: usize, + C1: *const secp256k1_pubkey, + C2_vec: *const secp256k1_pubkey, + Pk_vec: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Verifies the proof of equality with shared randomness."] + pub fn secp256k1_mpt_verify_equality_shared_r( + ctx: *const secp256k1_context, + proof: *const ::std::os::raw::c_uchar, + n: usize, + C1: *const secp256k1_pubkey, + C2_vec: *const secp256k1_pubkey, + Pk_vec: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_bulletproof_prove_agg( + ctx: *const secp256k1_context, + proof_out: *mut ::std::os::raw::c_uchar, + proof_len: *mut usize, + values: *const u64, + blindings_flat: *const ::std::os::raw::c_uchar, + m: usize, + pk_base: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_bulletproof_verify_agg( + ctx: *const secp256k1_context, + G_vec: *const secp256k1_pubkey, + H_vec: *const secp256k1_pubkey, + proof: *const ::std::os::raw::c_uchar, + proof_len: usize, + commitment_C_vec: *const secp256k1_pubkey, + m: usize, + pk_base: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generate a compact AND-composed sigma proof for standard EC-ElGamal.\n\n proof_out must point to a buffer of SECP256K1_COMPACT_STANDARD_PROOF_SIZE\n bytes. context_id is an optional 32-byte transaction context (may be NULL)."] + pub fn secp256k1_compact_standard_prove( + ctx: *const secp256k1_context, + proof_out: *mut ::std::os::raw::c_uchar, + amount: u64, + balance: u64, + r_shared: *const ::std::os::raw::c_uchar, + sk_A: *const ::std::os::raw::c_uchar, + r_b: *const ::std::os::raw::c_uchar, + n: usize, + C1: *const secp256k1_pubkey, + C2_vec: *const secp256k1_pubkey, + Pk_vec: *const secp256k1_pubkey, + PC_m: *const secp256k1_pubkey, + pk_A: *const secp256k1_pubkey, + PC_b: *const secp256k1_pubkey, + B1: *const secp256k1_pubkey, + B2: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verify a compact AND-composed sigma proof for standard EC-ElGamal.\n\n Returns 1 if the proof is valid, 0 otherwise."] + pub fn secp256k1_compact_standard_verify( + ctx: *const secp256k1_context, + proof: *const ::std::os::raw::c_uchar, + n: usize, + C1: *const secp256k1_pubkey, + C2_vec: *const secp256k1_pubkey, + Pk_vec: *const secp256k1_pubkey, + PC_m: *const secp256k1_pubkey, + pk_A: *const secp256k1_pubkey, + PC_b: *const secp256k1_pubkey, + B1: *const secp256k1_pubkey, + B2: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_compact_clawback_prove( + ctx: *const secp256k1_context, + proof_out: *mut ::std::os::raw::c_uchar, + amount: u64, + sk_iss: *const ::std::os::raw::c_uchar, + P_iss: *const secp256k1_pubkey, + C1: *const secp256k1_pubkey, + C2: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_compact_clawback_verify( + ctx: *const secp256k1_context, + proof: *const ::std::os::raw::c_uchar, + amount: u64, + P_iss: *const secp256k1_pubkey, + C1: *const secp256k1_pubkey, + C2: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_compact_convertback_prove( + ctx: *const secp256k1_context, + proof_out: *mut ::std::os::raw::c_uchar, + balance: u64, + sk_A: *const ::std::os::raw::c_uchar, + rho: *const ::std::os::raw::c_uchar, + pk_A: *const secp256k1_pubkey, + B1: *const secp256k1_pubkey, + B2: *const secp256k1_pubkey, + PC_b: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn secp256k1_compact_convertback_verify( + ctx: *const secp256k1_context, + proof: *const ::std::os::raw::c_uchar, + pk_A: *const secp256k1_pubkey, + B1: *const secp256k1_pubkey, + B2: *const secp256k1_pubkey, + PC_b: *const secp256k1_pubkey, + context_id: *const ::std::os::raw::c_uchar, + ) -> ::std::os::raw::c_int; +} +#[doc = " @brief Represents a unique 24-byte MPT issuance ID."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct mpt_issuance_id { + pub bytes: [u8; 24usize], +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of mpt_issuance_id"][::std::mem::size_of::() - 24usize]; + ["Alignment of mpt_issuance_id"][::std::mem::align_of::() - 1usize]; + ["Offset of field: mpt_issuance_id::bytes"] + [::std::mem::offset_of!(mpt_issuance_id, bytes) - 0usize]; +}; +#[doc = " @brief Represents a 20-byte account ID.\n\n - bytes: Raw 20-byte array containing the AccountID."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct account_id { + pub bytes: [u8; 20usize], +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of account_id"][::std::mem::size_of::() - 20usize]; + ["Alignment of account_id"][::std::mem::align_of::() - 1usize]; + ["Offset of field: account_id::bytes"][::std::mem::offset_of!(account_id, bytes) - 0usize]; +}; +#[doc = " @brief Represents a participant in a Confidential Send transaction.\n\n - pubkey: The 33-byte compressed secp256k1 public key.\n - ciphertext: The 66-byte ElGamal encrypted amount."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct mpt_confidential_participant { + pub pubkey: [u8; 33usize], + pub ciphertext: [u8; 66usize], +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of mpt_confidential_participant"] + [::std::mem::size_of::() - 99usize]; + ["Alignment of mpt_confidential_participant"] + [::std::mem::align_of::() - 1usize]; + ["Offset of field: mpt_confidential_participant::pubkey"] + [::std::mem::offset_of!(mpt_confidential_participant, pubkey) - 0usize]; + ["Offset of field: mpt_confidential_participant::ciphertext"] + [::std::mem::offset_of!(mpt_confidential_participant, ciphertext) - 33usize]; +}; +#[doc = " @brief Parameters required to generate a Pedersen Linkage Proof.\n\n - pedersen_commitment: The 64-byte Pedersen commitment.\n - amount: The actual numeric value being committed.\n - ciphertext: The 66-byte buffer containing the encrypted amount.\n - blinding_factor: The 32-byte secret value used to blind the commitment."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct mpt_pedersen_proof_params { + pub pedersen_commitment: [u8; 33usize], + pub amount: u64, + pub ciphertext: [u8; 66usize], + pub blinding_factor: [u8; 32usize], +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of mpt_pedersen_proof_params"] + [::std::mem::size_of::() - 152usize]; + ["Alignment of mpt_pedersen_proof_params"] + [::std::mem::align_of::() - 8usize]; + ["Offset of field: mpt_pedersen_proof_params::pedersen_commitment"] + [::std::mem::offset_of!(mpt_pedersen_proof_params, pedersen_commitment) - 0usize]; + ["Offset of field: mpt_pedersen_proof_params::amount"] + [::std::mem::offset_of!(mpt_pedersen_proof_params, amount) - 40usize]; + ["Offset of field: mpt_pedersen_proof_params::ciphertext"] + [::std::mem::offset_of!(mpt_pedersen_proof_params, ciphertext) - 48usize]; + ["Offset of field: mpt_pedersen_proof_params::blinding_factor"] + [::std::mem::offset_of!(mpt_pedersen_proof_params, blinding_factor) - 114usize]; +}; +unsafe extern "C" { + #[doc = " @brief Returns a globally shared secp256k1 context."] + pub fn mpt_secp256k1_context() -> *mut secp256k1_context; +} +unsafe extern "C" { + #[doc = " @brief Context Hash for ConfidentialMPTConvert."] + pub fn mpt_get_convert_context_hash( + account: account_id, + iss: mpt_issuance_id, + sequence: u32, + out_hash: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Context Hash for ConfidentialMPTConvertBack."] + pub fn mpt_get_convert_back_context_hash( + acc: account_id, + iss: mpt_issuance_id, + seq: u32, + ver: u32, + out_hash: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Context Hash for ConfidentialMPTSend."] + pub fn mpt_get_send_context_hash( + acc: account_id, + iss: mpt_issuance_id, + seq: u32, + dest: account_id, + ver: u32, + out_hash: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Context Hash for ConfidentialMPTClawback."] + pub fn mpt_get_clawback_context_hash( + acc: account_id, + iss: mpt_issuance_id, + seq: u32, + holder: account_id, + out_hash: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Parses a 66-byte buffer into two internal secp256k1 public keys.\n @param buffer [in] 66-byte buffer containing two points.\n @param out1 [out] First internal public key (C1).\n @param out2 [out] Second internal public key (C2).\n @return true on success, false if parsing fails."] + pub fn mpt_make_ec_pair( + buffer: *const u8, + out1: *mut secp256k1_pubkey, + out2: *mut secp256k1_pubkey, + ) -> bool; +} +unsafe extern "C" { + #[doc = " @brief Serializes two internal secp256k1 public keys into a 66-byte buffer.\n @param in1 [in] Internal format of the first point (C1).\n @param in2 [in] Internal format of the second point (C2).\n @param out [out] 66-byte buffer to write the serialized points.\n @return true if both points were valid and successfully serialized, false otherwise."] + pub fn mpt_serialize_ec_pair( + in1: *const secp256k1_pubkey, + in2: *const secp256k1_pubkey, + out: *mut u8, + ) -> bool; +} +unsafe extern "C" { + #[doc = " @brief Generates a new Secp256k1 ElGamal keypair.\n @param out_privkey [out] A 32-byte buffer for private key.\n @param out_pubkey [out] A 33-byte buffer for public key.\n @return 0 on success, -1 on failure."] + pub fn mpt_generate_keypair(out_privkey: *mut u8, out_pubkey: *mut u8) + -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates a 32-byte blinding factor.\n @param out_factor [out] A 32-byte buffer to store the blinding factor.\n @return 0 on success, -1 on failure."] + pub fn mpt_generate_blinding_factor(out_factor: *mut u8) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Encrypts an uint64 amount using an ElGamal public key.\n @param amount [in] The integer value to encrypt.\n @param pubkey [in] The 33-byte public key.\n @param blinding_factor [in] The 32-byte random blinding factor (scalar r).\n @param out_ciphertext [out] A 66-byte buffer to store the resulting ciphertext (C1, C2).\n @return 0 on success, -1 on failure."] + pub fn mpt_encrypt_amount( + amount: u64, + pubkey: *const u8, + blinding_factor: *const u8, + out_ciphertext: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Decrypts an MPT amount from a ciphertext pair.\n @param ciphertext [in] A 66-byte buffer containing the two points (C1, C2).\n @param privkey [in] The 32-byte private key.\n @param out_amount [out] Pointer to store the decrypted uint64_t amount.\n @return 0 on success, -1 on failure."] + pub fn mpt_decrypt_amount( + ciphertext: *const u8, + privkey: *const u8, + out_amount: *mut u64, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates a Schnorr Proof of Knowledge for a Confidential MPT conversion.\n\n This proof is used in 'ConfidentialMPTConvert' transactions to prove the\n sender possesses the private key associated with the account, binding it\n to the specific transaction via the ctx_hash.\n\n @param pubkey [in] 33-byte public key of the account.\n @param privkey [in] 32-byte private key of the account.\n @param ctx_hash [in] 32-byte hash of the transaction (challenge).\n @param out_proof [out] 64-byte buffer to store the compact Schnorr proof.\n @return 0 on success, -1 on failure."] + pub fn mpt_get_convert_proof( + pubkey: *const u8, + privkey: *const u8, + ctx_hash: *const u8, + out_proof: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Computes a Pedersen Commitment point for Confidential MPT.\n @param amount [in] The 64-bit unsigned integer value to commit.\n @param blinding_factor [in] A 32-byte secret scalar (rho) used to hide the amount.\n @param out_commitment [out] A 33-byte buffer to store the commitment"] + pub fn mpt_get_pedersen_commitment( + amount: u64, + blinding_factor: *const u8, + out_commitment: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates proof for ConfidentialMPTSend.\n\n Produces a compact AND-composed sigma proof (192 bytes) that simultaneously\n proves ciphertext equality, Pedersen commitment linkage, and balance ownership\n under a single Fiat-Shamir challenge, followed by an aggregated Bulletproof\n range proof (754 bytes). Total proof size is fixed at 946 bytes.\n\n pc_m must be computed as m*G + r*H using tx_blinding_factor as the blinding\n factor (not an independent scalar), since the compact sigma proof binds pc_m\n to the ciphertext randomness r.\n\n @param priv [in] The sender's 32-byte private key.\n @param pub [in] The sender's 33-byte public key.\n @param amount [in] The amount being sent.\n @param participants [in] List of participants, including Sender, Dest, Issuer,\n Auditor(optional).\n @param n_participants [in] Number of participants (3 or 4).\n @param tx_blinding_factor [in] The ElGamal randomness r (also blinding factor for pc_m).\n @param context_hash [in] The 32-byte context hash.\n @param amount_commitment [in] Pedersen commitment pc_m = m*G + r*H.\n @param balance_params [in] Includes pedersen_commitment (pc_b), amount (balance),\n blinding_factor (rho), and ciphertext (b1||b2).\n @param out_proof [out] Buffer to receive the proof blob.\n @param out_len [in/out] In: capacity (must be >= 946). Out: bytes written.\n @return 0 on success, -1 on failure."] + pub fn mpt_get_confidential_send_proof( + priv_: *const u8, + pub_: *const u8, + amount: u64, + participants: *const mpt_confidential_participant, + n_participants: usize, + tx_blinding_factor: *const u8, + context_hash: *const u8, + amount_commitment: *const u8, + balance_params: *const mpt_pedersen_proof_params, + out_proof: *mut u8, + out_len: *mut usize, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates proof for ConfidentialMPTConvertBack.\n\n Produces a compact AND-composed sigma proof (128 bytes) over the balance\n witness (b, rho, priv), followed by a single Bulletproof range proof (688\n bytes) over the remainder commitment pc_rem = pc_b - m*G.\n Total proof size: 816 bytes (SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE +\n kMPT_SINGLE_BULLETPROOF_SIZE).\n\n @param priv [in] The holder's 32-byte private key.\n @param pub [in] The holder's 33-byte public key.\n @param context_hash [in] The 32-byte context hash binding the proof to the transaction.\n @param amount [in] The publicly revealed conversion amount m.\n @param params [in] Includes pedersen_commitment (pc_b), blinding_factor (rho),\n amount (balance b), and ciphertext (b1||b2).\n @param out_proof [out] 816-byte buffer for the compact sigma proof and range proof.\n @return 0 on success, -1 on failure."] + pub fn mpt_get_convert_back_proof( + priv_: *const u8, + pub_: *const u8, + context_hash: *const u8, + amount: u64, + params: *const mpt_pedersen_proof_params, + out_proof: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generates proof for ConfidentialMPTClawback.\n @param priv [in] The issuer's 32-byte private key.\n @param pub [in] The issuer's 33-byte compressed public key.\n @param context_hash [in] The 32-byte context hash binding the proof to the transaction.\n @param amount [in] The publicly known amount to be clawed back.\n @param ciphertext [in] The 66-byte sfIssuerEncryptedBalance blob from the ledger.\n @param out_proof [out] 64-byte buffer for the compact sigma proof.\n @return 0 on success, -1 on failure."] + pub fn mpt_get_clawback_proof( + priv_: *const u8, + pub_: *const u8, + context_hash: *const u8, + amount: u64, + ciphertext: *const u8, + out_proof: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verifies that the ElGamal ciphertexts held by all participants encrypt\n the same revealed plaintext amount under the given blinding factor.\n\n @param amount [in] Actual numeric amount to verify against.\n @param blinding_factor [in] The ElGamal randomness r used to produce all ciphertexts.\n @param holder [in] Holder's public key and ciphertext.\n @param issuer [in] Issuer's public key and ciphertext.\n @param auditor [in] Auditor's public key and ciphertext, optional.\n @return 0 on success, -1 on failure."] + pub fn mpt_verify_revealed_amount( + amount: u64, + blinding_factor: *const u8, + holder: *const mpt_confidential_participant, + issuer: *const mpt_confidential_participant, + auditor: *const mpt_confidential_participant, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verify proof for ConfidentialMPTConvert.\n\n Proves that the sender possesses the private key for the provided public key.\n\n @param proof [in] The 64-byte compact Schnorr proof.\n @param pubkey [in] The 33-byte compressed ElGamal public key.\n @param context_hash [in] The 32-byte transaction context hash.\n @return 0 on success, -1 on failure."] + pub fn mpt_verify_convert_proof( + proof: *const u8, + pubkey: *const u8, + context_hash: *const u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verify proof for ConfidentialMPTConvertBack\n\n Proves that the hidden balance matches the commitment and that\n subtracting the transparent amount results in a non-negative balance.\n\n @param proof [in] 816-byte proof blob (compact sigma || Bulletproof).\n @param pubkey [in] The holder's 33-byte ElGamal public key.\n @param ciphertext [in] The holder's 66-byte balance ciphertext.\n @param balance_commitment [in] The 33-byte Pedersen commitment to the balance.\n @param amount [in] The publicly revealed conversion amount m.\n @param context_hash [in] The 32-byte transaction context hash.\n @return 0 on success, -1 on failure."] + pub fn mpt_verify_convert_back_proof( + proof: *const u8, + pubkey: *const u8, + ciphertext: *const u8, + balance_commitment: *const u8, + amount: u64, + context_hash: *const u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verify proof for ConfidentialMPTSend.\n\n Verifies the compact AND-composed sigma proof (first 192 bytes) that proves\n ciphertext correctness, Pedersen commitment linkage, and balance ownership,\n followed by an aggregated Bulletproof range proof (next 754 bytes).\n Proof size is fixed at 946 bytes.\n\n @param proof [in] 946-byte proof blob (compact sigma || Bulletproof).\n @param participants [in] List of participants' public keys and ciphertexts.\n participants[0] is the sender.\n @param n_participants [in] Number of participants (3 or 4).\n @param sender_spending_ciphertext [in] The sender's on-ledger balance ciphertext (b1||b2).\n @param amount_commitment [in] Pedersen commitment pc_m to the transfer amount.\n @param balance_commitment [in] Pedersen commitment pc_b to the sender's balance.\n @param context_hash [in] The 32-byte transaction context hash.\n @return 0 on success, -1 on failure."] + pub fn mpt_verify_send_proof( + proof: *const u8, + participants: *const mpt_confidential_participant, + n_participants: u8, + sender_spending_ciphertext: *const u8, + amount_commitment: *const u8, + balance_commitment: *const u8, + context_hash: *const u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verify proof for ConfidentialMPTClawback.\n\n Proves that a ciphertext, when decrypted by the issuer, results in exactly the plaintext amount\n specified in the transaction.\n\n @param proof [in] The 64-byte compact sigma proof.\n @param amount [in] The publicly known amount to be clawed back.\n @param pubkey [in] The issuer's 33-byte compressed public key.\n @param ciphertext [in] The 66-byte sfIssuerEncryptedBalance blob associated with the holder's\n account on the ledger.\n @param context_hash [in] The 32-byte transaction context hash.\n @return 0 on success, -1 on failure."] + pub fn mpt_verify_clawback_proof( + proof: *const u8, + amount: u64, + pubkey: *const u8, + ciphertext: *const u8, + context_hash: *const u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Helper function to substract a transparent amount from a hidden commitment.\n\n @param commitment_in [in] The 33-byte starting Pedersen commitment.\n @param amount [in] The integer amount to subtract.\n @param commitment_out [out] The resulting 33-byte remainder commitment.\n @return 0 on success, -1 on failure."] + pub fn mpt_compute_convert_back_remainder( + commitment_in: *const u8, + amount: u64, + commitment_out: *mut u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Generic verifier for aggregated Bulletproofs (Range Proofs).\n\n @param proof [in] The serialized Bulletproof buffer.\n @param proof_len [in] The length of the proof buffer in bytes.\n @param compressed_commitments [in] An array of pointers to the 33-byte Pedersen commitments.\n @param m [in] The number of commitments to verify.\n @param context_hash [in] The 32-byte context hash binding the proof to the transaction.\n @return 0 on success, -1 on failure."] + pub fn mpt_verify_aggregated_bulletproof( + proof: *const u8, + proof_len: usize, + compressed_commitments: *mut *const u8, + m: usize, + context_hash: *const u8, + ) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " @brief Verifies that the sending amount and remaining balance reside within the valid range from\n 0 to 2^64-1.\n\n @param ctx [in] secp256k1-zkp context.\n @param proof [in] 754-byte Double Bulletproof.\n @param amount_commitment [in] Pedersen commitment to the transfer amount.\n @param balance_commitment [in] Pedersen commitment to the sender's total balance.\n @param context_hash [in] 32-byte transaction context hash.\n @return 0 on success, -1 on failure."] + pub fn mpt_verify_send_range_proof( + proof: *const u8, + amount_commitment: *const u8, + balance_commitment: *const u8, + context_hash: *const u8, + ) -> ::std::os::raw::c_int; +} \ No newline at end of file diff --git a/internal/mpt-crypto-sys/src/lib.rs b/internal/mpt-crypto-sys/src/lib.rs new file mode 100644 index 00000000..b54f448a --- /dev/null +++ b/internal/mpt-crypto-sys/src/lib.rs @@ -0,0 +1,18 @@ +//! Low-level FFI bindings to the [mpt-crypto](https://github.com/XRPLF/mpt-crypto) +//! C library, which implements the cryptographic primitives for +//! [XLS-0096 Confidential MPTs](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0096-confidential-mpt). +//! +//! **Not a stable public API.** Use the high-level `mpt-crypto` wrapper crate +//! instead — that crate provides idiomatic Rust types, error handling, and +//! secret-scrubbing. +//! +//! See [`build.rs`] for how the native library is located on the filesystem. + +#![allow( + non_upper_case_globals, + non_camel_case_types, + non_snake_case, + dead_code +)] + +include!("bindings.rs"); diff --git a/internal/mpt-crypto-sys/tests/smoke.rs b/internal/mpt-crypto-sys/tests/smoke.rs new file mode 100644 index 00000000..36a9ba78 --- /dev/null +++ b/internal/mpt-crypto-sys/tests/smoke.rs @@ -0,0 +1,198 @@ +//! Runtime smoke test: exercises the full FFI chain — the dynamic linker +//! finds `libmpt-crypto.{dylib,so,dll}` (via rpath or PATH), symbols resolve, +//! and a C function call round-trips with sensible output. +//! +//! If this passes, the scaffold is wired correctly end-to-end. + +use mpt_crypto_sys as sys; + +#[test] +fn creates_and_destroys_a_secp256k1_context() { + // SAFETY: secp256k1_context_create returns a valid pointer for valid + // flag combinations, or null on allocation failure. We pass + // SIGN|VERIFY, which is a valid combination documented in the + // upstream secp256k1.h. + unsafe { + let flags = sys::SECP256K1_CONTEXT_SIGN | sys::SECP256K1_CONTEXT_VERIFY; + let ctx = sys::secp256k1_context_create(flags); + assert!(!ctx.is_null(), "context_create returned null"); + sys::secp256k1_context_destroy(ctx); + } +} + +#[test] +fn generates_an_elgamal_keypair_with_nonzero_bytes() { + // Validates that the upstream library was built with a working RNG + // (OpenSSL statically linked inside the dylib). + let mut privkey = [0u8; 32]; + let mut pubkey = [0u8; 33]; + + // SAFETY: mpt_generate_keypair writes 32 bytes to privkey and 33 bytes + // to pubkey; buffer sizes match upstream's constants + // kMPT_PRIVKEY_SIZE and kMPT_PUBKEY_SIZE. + let rc = unsafe { sys::mpt_generate_keypair(privkey.as_mut_ptr(), pubkey.as_mut_ptr()) }; + assert_eq!(rc, 0, "mpt_generate_keypair returned {rc}"); + + // Non-deterministic: all zeros would indicate the RNG did nothing. + assert!(privkey.iter().any(|&b| b != 0), "privkey is all zeros"); + assert!(pubkey.iter().any(|&b| b != 0), "pubkey is all zeros"); + + // Compressed secp256k1 pubkey starts with 0x02 or 0x03. + assert!( + pubkey[0] == 0x02 || pubkey[0] == 0x03, + "pubkey prefix byte = 0x{:02x}, expected 0x02 or 0x03", + pubkey[0] + ); +} + +// ───────────────────────────────────────────────────────────────────────── +// Constant-documentation tests +// +// These tests are the executable form of the spec-vs-implementation +// agreement. They run during `cargo test -p mpt-crypto-sys` and fail +// loudly if the linked libmpt-crypto ever drifts from what XLS-0096 says. +// Treat the assertion strings as documentation: each one says where the +// number comes from in the spec. +// ───────────────────────────────────────────────────────────────────────── + +/// The four atomic sigma-proof sizes defined in `secp256k1_mpt.h`. +/// +/// These are the irreducible primitives — every higher-level proof bundle +/// is a composition of these and the Bulletproof base sizes. +#[test] +fn sigma_proof_base_sizes() { + // §7.2 / §A.7 — Schnorr Proof of Knowledge for holder-key registration. + assert_eq!(sys::SECP256K1_POK_SK_PROOF_SIZE, 64); + + // §5.4 / §8.2 — compact AND-composed sigma in ConfidentialMPTSend. + // Witnesses: r, m, sk_A, rho, b (5 scalars, encoded as Z_q^6 = 192 B). + assert_eq!(sys::SECP256K1_COMPACT_STANDARD_PROOF_SIZE, 192); + + // §5.4 / §10.3 — compact sigma in ConfidentialMPTConvertBack. + // Witnesses: b, sk_A, rho (3 scalars, encoded as Z_q^4 = 128 B). + assert_eq!(sys::SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE, 128); + + // §5.4 / §11.2 — compact sigma in ConfidentialMPTClawback. + // Witnesses: sk_iss (1 scalar, encoded as Z_q^2 = 64 B). + assert_eq!(sys::SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE, 64); +} + +/// The three Bulletproof base sizes defined in `mpt_utility.h`. +/// +/// Aggregated proves m=2 values in [0, 2^64) (used in Send: amount + remainder). +/// Single proves m=1 value in [0, 2^64) (used in ConvertBack: post-withdraw remainder). +#[test] +fn bulletproof_base_sizes() { + // §5.4 / §10.3 — single 64-bit range proof (ConvertBack). + assert_eq!(sys::kMPT_SINGLE_BULLETPROOF_SIZE, 688); + + // §5.4 / §8.2 / §14.1 — aggregated proof for two 64-bit values (Send). + assert_eq!(sys::kMPT_DOUBLE_BULLETPROOF_SIZE, 754); +} + +/// The utility layer's `kMPT_SCHNORR_PROOF_SIZE` aliases the primitive +/// layer's `SECP256K1_POK_SK_PROOF_SIZE`. Both identify the 64 B Schnorr PoK +/// used by ConfidentialMPTConvert. +/// +/// If the two ever diverge, one of the layers has a stale definition and +/// the bindings will silently agree with the wrong one. +#[test] +fn schnorr_proof_aliases_are_consistent() { + assert_eq!( + sys::kMPT_SCHNORR_PROOF_SIZE, + sys::SECP256K1_POK_SK_PROOF_SIZE + ); + assert_eq!(sys::kMPT_SCHNORR_PROOF_SIZE, 64); +} + +/// Composed proof sizes per XLS-0096 §5.4 / §14.1. +/// +/// These are the totals the safe-wrapper `*Proof` newtypes ([u8; N]) hardcode. +/// Each total is `sigma_size + range_proof_size` (or just one of the two). +/// Asserting them by addition ties the wrappers to the base constants — +/// drift in either base value is caught here. +#[test] +fn total_proof_sizes_per_xls_0096() { + // Convert: just the Schnorr PoK. §7.2. + assert_eq!(64, sys::SECP256K1_POK_SK_PROOF_SIZE); + + // Send: 192 (compact sigma) + 754 (aggregated Bulletproof) = 946. §8.2. + let send_total = sys::SECP256K1_COMPACT_STANDARD_PROOF_SIZE + sys::kMPT_DOUBLE_BULLETPROOF_SIZE; + assert_eq!( + send_total, 946, + "Send proof composes to 192 + 754 = 946 per §5.4 / §14.1" + ); + + // ConvertBack: 128 (compact sigma) + 688 (single Bulletproof) = 816. §10.3. + let convert_back_total = + sys::SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE + sys::kMPT_SINGLE_BULLETPROOF_SIZE; + assert_eq!( + convert_back_total, 816, + "ConvertBack proof composes to 128 + 688 = 816 per §5.4 / §10.3" + ); + + // Clawback: just the compact sigma. §11.2. + assert_eq!(64, sys::SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE); + + // MergeInbox is intentionally absent — proof-free per §9 / §A.2. +} + +/// Wire sizes for cryptographic blobs — the field types every confidential +/// transaction carries. +/// +/// These match the XLS-0096 transaction-field tables (§7.2, §8.2, §10.3, +/// §11.2): every `Blob` field documented as N bytes corresponds to one of +/// these constants. +#[test] +fn wire_size_primitives() { + // Compressed secp256k1 public key = 33 bytes (1 prefix byte + 32-byte X). + // Used by HolderEncryptionKey / IssuerEncryptionKey / AuditorEncryptionKey. + assert_eq!(sys::kMPT_PUBKEY_SIZE, 33); + + // 32-byte secret scalar — Privkey, BlindingFactor, ContextHash all share. + assert_eq!(sys::kMPT_PRIVKEY_SIZE, 32); + assert_eq!(sys::kMPT_BLINDING_FACTOR_SIZE, 32); + assert_eq!(sys::kMPT_HALF_SHA_SIZE, 32); // SHA-256 / 2 (truncated 32 B output) + + // ElGamal ciphertext = (R, S), each a compressed point. + // R alone = 33 B; (R, S) total = 66 B. + // Used as HolderEncryptedAmount / IssuerEncryptedAmount / AuditorEncryptedAmount, + // and as on-ledger CB_S, CB_IN, IssuerEncryptedBalance, AuditorEncryptedBalance. + assert_eq!(sys::kMPT_ELGAMAL_CIPHER_SIZE, 33); + assert_eq!(sys::kMPT_ELGAMAL_TOTAL_SIZE, 66); + assert_eq!( + sys::kMPT_ELGAMAL_TOTAL_SIZE, + sys::kMPT_ELGAMAL_CIPHER_SIZE * 2, + "ElGamal ciphertext is exactly two compressed points" + ); + + // Pedersen commitment = single compressed point. + // Used for AmountCommitment and BalanceCommitment. + assert_eq!(sys::kMPT_PEDERSEN_COMMIT_SIZE, 33); +} + +/// XRPL ledger-identifier sizes used to build per-transaction context hashes. +#[test] +fn ledger_identifier_sizes() { + // 20-byte XRPL classic-address payload (RIPEMD-160 of a SHA-256). + assert_eq!(sys::kMPT_ACCOUNT_ID_SIZE, 20); + + // 24-byte MPTokenIssuanceID = 4 byte sequence || 20 byte issuer AccountID. + // Defined by XLS-33; XLS-0096 reuses verbatim. + assert_eq!(sys::kMPT_ISSUANCE_ID_SIZE, 24); +} + +/// Transaction-type IDs assigned to confidential MPT transactions. +/// +/// These numbers are the contract between the C library, rippled's +/// transaction-dispatch table, and any client serializing `TransactionType` +/// to wire format. If they ever drift, every binary-codec round-trip breaks. +#[test] +fn transaction_type_ids_match_xls_0096() { + // §4.1 of the spec assigns these directly. + assert_eq!(sys::ttCONFIDENTIAL_MPT_CONVERT, 85); + assert_eq!(sys::ttCONFIDENTIAL_MPT_MERGE_INBOX, 86); + assert_eq!(sys::ttCONFIDENTIAL_MPT_CONVERT_BACK, 87); + assert_eq!(sys::ttCONFIDENTIAL_MPT_SEND, 88); + assert_eq!(sys::ttCONFIDENTIAL_MPT_CLAWBACK, 89); +} diff --git a/internal/mpt-crypto-sys/vendor/include/secp256k1.h b/internal/mpt-crypto-sys/vendor/include/secp256k1.h new file mode 100644 index 00000000..b7ec6a22 --- /dev/null +++ b/internal/mpt-crypto-sys/vendor/include/secp256k1.h @@ -0,0 +1,929 @@ +#ifndef SECP256K1_H +#define SECP256K1_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** Unless explicitly stated all pointer arguments must not be NULL. + * + * The following rules specify the order of arguments in API calls: + * + * 1. Context pointers go first, followed by output arguments, combined + * output/input arguments, and finally input-only arguments. + * 2. Array lengths always immediately follow the argument whose length + * they describe, even if this violates rule 1. + * 3. Within the OUT/OUTIN/IN groups, pointers to data that is typically generated + * later go first. This means: signatures, public nonces, secret nonces, + * messages, public keys, secret keys, tweaks. + * 4. Arguments that are not data pointers go last, from more complex to less + * complex: function pointers, algorithm names, messages, void pointers, + * counts, flags, booleans. + * 5. Opaque data pointers follow the function pointer they are to be passed to. + */ + +/** Opaque data structure that holds context information + * + * The primary purpose of context objects is to store randomization data for + * enhanced protection against side-channel leakage. This protection is only + * effective if the context is randomized after its creation. See + * secp256k1_context_create for creation of contexts and + * secp256k1_context_randomize for randomization. + * + * A secondary purpose of context objects is to store pointers to callback + * functions that the library will call when certain error states arise. See + * secp256k1_context_set_error_callback as well as + * secp256k1_context_set_illegal_callback for details. Future library versions + * may use context objects for additional purposes. + * + * A constructed context can safely be used from multiple threads + * simultaneously, but API calls that take a non-const pointer to a context + * need exclusive access to it. In particular this is the case for + * secp256k1_context_destroy, secp256k1_context_preallocated_destroy, + * and secp256k1_context_randomize. + * + * Regarding randomization, either do it once at creation time (in which case + * you do not need any locking for the other calls), or use a read-write lock. + */ +typedef struct secp256k1_context_struct secp256k1_context; + +/** Opaque data structure that holds a parsed and valid public key. + * + * The exact representation of data inside is implementation defined and not + * guaranteed to be portable between different platforms or versions. It is + * however guaranteed to be 64 bytes in size, and can be safely copied/moved. + * If you need to convert to a format suitable for storage or transmission, + * use secp256k1_ec_pubkey_serialize and secp256k1_ec_pubkey_parse. To + * compare keys, use secp256k1_ec_pubkey_cmp. + */ +typedef struct secp256k1_pubkey { + unsigned char data[64]; +} secp256k1_pubkey; + +/** Opaque data structure that holds a parsed ECDSA signature. + * + * The exact representation of data inside is implementation defined and not + * guaranteed to be portable between different platforms or versions. It is + * however guaranteed to be 64 bytes in size, and can be safely copied/moved. + * If you need to convert to a format suitable for storage, transmission, or + * comparison, use the secp256k1_ecdsa_signature_serialize_* and + * secp256k1_ecdsa_signature_parse_* functions. + */ +typedef struct secp256k1_ecdsa_signature { + unsigned char data[64]; +} secp256k1_ecdsa_signature; + +/** A pointer to a function to deterministically generate a nonce. + * + * Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. + * Out: nonce32: pointer to a 32-byte array to be filled by the function. + * In: msg32: the 32-byte message hash being verified (will not be NULL) + * key32: pointer to a 32-byte secret key (will not be NULL) + * algo16: pointer to a 16-byte array describing the signature + * algorithm (will be NULL for ECDSA for compatibility). + * data: Arbitrary data pointer that is passed through. + * attempt: how many iterations we have tried to find a nonce. + * This will almost always be 0, but different attempt values + * are required to result in a different nonce. + * + * Except for test cases, this function should compute some cryptographic hash of + * the message, the algorithm, the key and the attempt. + */ +typedef int (*secp256k1_nonce_function)( + unsigned char *nonce32, + const unsigned char *msg32, + const unsigned char *key32, + const unsigned char *algo16, + void *data, + unsigned int attempt +); + +# if !defined(SECP256K1_GNUC_PREREQ) +# if defined(__GNUC__)&&defined(__GNUC_MINOR__) +# define SECP256K1_GNUC_PREREQ(_maj,_min) \ + ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) +# else +# define SECP256K1_GNUC_PREREQ(_maj,_min) 0 +# endif +# endif + +/* When this header is used at build-time the SECP256K1_BUILD define needs to be set + * to correctly setup export attributes and nullness checks. This is normally done + * by secp256k1.c but to guard against this header being included before secp256k1.c + * has had a chance to set the define (e.g. via test harnesses that just includes + * secp256k1.c) we set SECP256K1_NO_BUILD when this header is processed without the + * BUILD define so this condition can be caught. + */ +#ifndef SECP256K1_BUILD +# define SECP256K1_NO_BUILD +#endif + +/* Symbol visibility. */ +#if !defined(SECP256K1_API) && defined(SECP256K1_NO_API_VISIBILITY_ATTRIBUTES) + /* The user has requested that we don't specify visibility attributes in + * the public API. + * + * Since all our non-API declarations use the static qualifier, this means + * that the user can use -fvisibility= to set the visibility of the + * API symbols. For instance, -fvisibility=hidden can be useful *even for + * the API symbols*, e.g., when building a static library which is linked + * into a shared library, and the latter should not re-export the + * libsecp256k1 API. + * + * While visibility is a concept that applies only to shared libraries, + * setting visibility will still make a difference when building a static + * library: the visibility settings will be stored in the static library, + * solely for the potential case that the static library will be linked into + * a shared library. In that case, the stored visibility settings will + * resurface and be honored for the shared library. */ +# define SECP256K1_API extern +#endif +#if !defined(SECP256K1_API) +# if defined(SECP256K1_BUILD) + /* On Windows, assume a shared library only if explicitly requested. + * 1. If using Libtool, it defines DLL_EXPORT automatically. + * 2. In other cases, SECP256K1_DLL_EXPORT must be defined. */ +# if defined(_WIN32) && (defined(SECP256K1_DLL_EXPORT) || defined(DLL_EXPORT)) + /* GCC for Windows (e.g., MinGW) accepts the __declspec syntax for + * MSVC compatibility. A __declspec declaration implies (but is not + * exactly equivalent to) __attribute__ ((visibility("default"))), + * and so we actually want __declspec even on GCC, see "Microsoft + * Windows Function Attributes" in the GCC manual and the + * recommendations in https://gcc.gnu.org/wiki/Visibility . */ +# define SECP256K1_API extern __declspec(dllexport) + /* Avoid __attribute__ ((visibility("default"))) on Windows to get rid + * of warnings when compiling with -flto due to a bug in GCC, see + * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116478 . */ +# elif !defined(_WIN32) && defined (__GNUC__) && (__GNUC__ >= 4) +# define SECP256K1_API extern __attribute__ ((visibility("default"))) +# else +# define SECP256K1_API extern +# endif +# else + /* On Windows, SECP256K1_STATIC must be defined when consuming + * libsecp256k1 as a static library. Note that SECP256K1_STATIC is a + * "consumer-only" macro, and it has no meaning when building + * libsecp256k1. */ +# if defined(_WIN32) && !defined(SECP256K1_STATIC) +# define SECP256K1_API extern __declspec(dllimport) +# else +# define SECP256K1_API extern +# endif +# endif +#endif + +/* Warning attributes + * NONNULL is not used if SECP256K1_BUILD is set to avoid the compiler optimizing out + * some paranoid null checks. */ +# if defined(__GNUC__) && SECP256K1_GNUC_PREREQ(3, 4) +# define SECP256K1_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) +# else +# define SECP256K1_WARN_UNUSED_RESULT +# endif +# if !defined(SECP256K1_BUILD) && defined(__GNUC__) && SECP256K1_GNUC_PREREQ(3, 4) +# define SECP256K1_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x))) +# else +# define SECP256K1_ARG_NONNULL(_x) +# endif + +/* Attribute for marking functions, types, and variables as deprecated */ +#if !defined(SECP256K1_BUILD) && defined(__has_attribute) +# if __has_attribute(__deprecated__) +# define SECP256K1_DEPRECATED(_msg) __attribute__ ((__deprecated__(_msg))) +# else +# define SECP256K1_DEPRECATED(_msg) +# endif +#else +# define SECP256K1_DEPRECATED(_msg) +#endif + +/* All flags' lower 8 bits indicate what they're for. Do not use directly. */ +#define SECP256K1_FLAGS_TYPE_MASK ((1 << 8) - 1) +#define SECP256K1_FLAGS_TYPE_CONTEXT (1 << 0) +#define SECP256K1_FLAGS_TYPE_COMPRESSION (1 << 1) +/* The higher bits contain the actual data. Do not use directly. */ +#define SECP256K1_FLAGS_BIT_CONTEXT_VERIFY (1 << 8) +#define SECP256K1_FLAGS_BIT_CONTEXT_SIGN (1 << 9) +#define SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY (1 << 10) +#define SECP256K1_FLAGS_BIT_COMPRESSION (1 << 8) + +/** Context flags to pass to secp256k1_context_create, secp256k1_context_preallocated_size, and + * secp256k1_context_preallocated_create. */ +#define SECP256K1_CONTEXT_NONE (SECP256K1_FLAGS_TYPE_CONTEXT) + +/** Deprecated context flags. These flags are treated equivalent to SECP256K1_CONTEXT_NONE. */ +#define SECP256K1_CONTEXT_VERIFY (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_VERIFY) +#define SECP256K1_CONTEXT_SIGN (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN) + +/* Testing flag. Do not use. */ +#define SECP256K1_CONTEXT_DECLASSIFY (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY) + +/** Flag to pass to secp256k1_ec_pubkey_serialize. */ +#define SECP256K1_EC_COMPRESSED (SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION) +#define SECP256K1_EC_UNCOMPRESSED (SECP256K1_FLAGS_TYPE_COMPRESSION) + +/** Prefix byte used to tag various encoded curvepoints for specific purposes */ +#define SECP256K1_TAG_PUBKEY_EVEN 0x02 +#define SECP256K1_TAG_PUBKEY_ODD 0x03 +#define SECP256K1_TAG_PUBKEY_UNCOMPRESSED 0x04 +#define SECP256K1_TAG_PUBKEY_HYBRID_EVEN 0x06 +#define SECP256K1_TAG_PUBKEY_HYBRID_ODD 0x07 + +/** A built-in constant secp256k1 context object with static storage duration, to be + * used in conjunction with secp256k1_selftest. + * + * This context object offers *only limited functionality* , i.e., it cannot be used + * for API functions that perform computations involving secret keys, e.g., signing + * and public key generation. If this restriction applies to a specific API function, + * it is mentioned in its documentation. See secp256k1_context_create if you need a + * full context object that supports all functionality offered by the library. + * + * It is highly recommended to call secp256k1_selftest before using this context. + */ +SECP256K1_API const secp256k1_context * const secp256k1_context_static; + +/** Deprecated alias for secp256k1_context_static. */ +SECP256K1_API const secp256k1_context * const secp256k1_context_no_precomp +SECP256K1_DEPRECATED("Use secp256k1_context_static instead"); + +/** Perform basic self tests (to be used in conjunction with secp256k1_context_static) + * + * This function performs self tests that detect some serious usage errors and + * similar conditions, e.g., when the library is compiled for the wrong endianness. + * This is a last resort measure to be used in production. The performed tests are + * very rudimentary and are not intended as a replacement for running the test + * binaries. + * + * It is highly recommended to call this before using secp256k1_context_static. + * It is not necessary to call this function before using a context created with + * secp256k1_context_create (or secp256k1_context_preallocated_create), which will + * take care of performing the self tests. + * + * If the tests fail, this function will call the default error callback to abort the + * program (see secp256k1_context_set_error_callback). + */ +SECP256K1_API void secp256k1_selftest(void); + + +/** Create a secp256k1 context object (in dynamically allocated memory). + * + * This function uses malloc to allocate memory. It is guaranteed that malloc is + * called at most once for every call of this function. If you need to avoid dynamic + * memory allocation entirely, see secp256k1_context_static and the functions in + * secp256k1_preallocated.h. + * + * Returns: pointer to a newly created context object. + * In: flags: Always set to SECP256K1_CONTEXT_NONE (see below). + * + * The only valid non-deprecated flag in recent library versions is + * SECP256K1_CONTEXT_NONE, which will create a context sufficient for all functionality + * offered by the library. All other (deprecated) flags will be treated as equivalent + * to the SECP256K1_CONTEXT_NONE flag. Though the flags parameter primarily exists for + * historical reasons, future versions of the library may introduce new flags. + * + * If the context is intended to be used for API functions that perform computations + * involving secret keys, e.g., signing and public key generation, then it is highly + * recommended to call secp256k1_context_randomize on the context before calling + * those API functions. This will provide enhanced protection against side-channel + * leakage, see secp256k1_context_randomize for details. + * + * Do not create a new context object for each operation, as construction and + * randomization can take non-negligible time. + */ +SECP256K1_API secp256k1_context *secp256k1_context_create( + unsigned int flags +) SECP256K1_WARN_UNUSED_RESULT; + +/** Copy a secp256k1 context object (into dynamically allocated memory). + * + * This function uses malloc to allocate memory. It is guaranteed that malloc is + * called at most once for every call of this function. If you need to avoid dynamic + * memory allocation entirely, see the functions in secp256k1_preallocated.h. + * + * Cloning secp256k1_context_static is not possible, and should not be emulated by + * the caller (e.g., using memcpy). Create a new context instead. + * + * Returns: pointer to a newly created context object. + * Args: ctx: pointer to a context to copy (not secp256k1_context_static). + */ +SECP256K1_API secp256k1_context *secp256k1_context_clone( + const secp256k1_context *ctx +) SECP256K1_ARG_NONNULL(1) SECP256K1_WARN_UNUSED_RESULT; + +/** Destroy a secp256k1 context object (created in dynamically allocated memory). + * + * The context pointer may not be used afterwards. + * + * The context to destroy must have been created using secp256k1_context_create + * or secp256k1_context_clone. If the context has instead been created using + * secp256k1_context_preallocated_create or secp256k1_context_preallocated_clone, the + * behaviour is undefined. In that case, secp256k1_context_preallocated_destroy must + * be used instead. + * + * Args: ctx: pointer to a context to destroy, constructed using + * secp256k1_context_create or secp256k1_context_clone + * (i.e., not secp256k1_context_static). + */ +SECP256K1_API void secp256k1_context_destroy( + secp256k1_context *ctx +) SECP256K1_ARG_NONNULL(1); + +/** Set a callback function to be called when an illegal argument is passed to + * an API call. It will only trigger for violations that are mentioned + * explicitly in the header. + * + * The philosophy is that these shouldn't be dealt with through a specific + * return value, as calling code should not have branches to deal with the case + * that this code itself is broken. + * + * On the other hand, during debug stage, one would want to be informed about + * such mistakes, and the default (crashing) may be inadvisable. Should this + * callback return instead of crashing, the return value and output arguments + * of the API function call are undefined. Moreover, the same API call may + * trigger the callback again in this case. + * + * When this function has not been called (or called with fun==NULL), then the + * default callback will be used. The library provides a default callback which + * writes the message to stderr and calls abort. This default callback can be + * replaced at link time if the preprocessor macro + * USE_EXTERNAL_DEFAULT_CALLBACKS is defined, which is the case if the build + * has been configured with --enable-external-default-callbacks (GNU Autotools) or + * -DSECP256K1_USE_EXTERNAL_DEFAULT_CALLBACKS=ON (CMake). Then the + * following two symbols must be provided to link against: + * - void secp256k1_default_illegal_callback_fn(const char *message, void *data); + * - void secp256k1_default_error_callback_fn(const char *message, void *data); + * The library may call a default callback even before a proper callback data + * pointer could have been set using secp256k1_context_set_illegal_callback or + * secp256k1_context_set_error_callback, e.g., when the creation of a context + * fails. In this case, the corresponding default callback will be called with + * the data pointer argument set to NULL. + * + * Args: ctx: pointer to a context object. + * In: fun: pointer to a function to call when an illegal argument is + * passed to the API, taking a message and an opaque pointer. + * (NULL restores the default callback.) + * data: the opaque pointer to pass to fun above, must be NULL for the + * default callback. + * + * See also secp256k1_context_set_error_callback. + */ +SECP256K1_API void secp256k1_context_set_illegal_callback( + secp256k1_context *ctx, + void (*fun)(const char *message, void *data), + const void *data +) SECP256K1_ARG_NONNULL(1); + +/** Set a callback function to be called when an internal consistency check + * fails. + * + * The default callback writes an error message to stderr and calls abort + * to abort the program. + * + * This can only trigger in case of a hardware failure, miscompilation, + * memory corruption, serious bug in the library, or other error that would + * result in undefined behaviour. It will not trigger due to mere + * incorrect usage of the API (see secp256k1_context_set_illegal_callback + * for that). After this callback returns, anything may happen, including + * crashing. + * + * Args: ctx: pointer to a context object. + * In: fun: pointer to a function to call when an internal error occurs, + * taking a message and an opaque pointer (NULL restores the + * default callback, see secp256k1_context_set_illegal_callback + * for details). + * data: the opaque pointer to pass to fun above, must be NULL for the + * default callback. + * + * See also secp256k1_context_set_illegal_callback. + */ +SECP256K1_API void secp256k1_context_set_error_callback( + secp256k1_context *ctx, + void (*fun)(const char *message, void *data), + const void *data +) SECP256K1_ARG_NONNULL(1); + +/** A pointer to a function implementing SHA256's internal compression function. + * + * This function processes one or more contiguous 64-byte message blocks and + * updates the internal SHA256 state accordingly. The function is not responsible + * for counting consumed blocks or bytes, nor for performing padding. + * + * In/Out: state: pointer to eight 32-bit words representing the current internal state; + * the state is updated in place. + * In: blocks64: pointer to concatenation of n_blocks blocks, of 64 bytes each. + * no alignment guarantees are made for this pointer. + * n_blocks: number of contiguous 64-byte blocks to process. + */ +typedef void (*secp256k1_sha256_compression_function)( + uint32_t *state, + const unsigned char *blocks64, + size_t n_blocks +); + +/** + * Set a callback function to override the internal SHA256 compression function. + * + * This installs a function to replace the built-in block-compression + * step used by the library's internal SHA256 implementation. + * The provided callback must exactly implement the effect of n_blocks + * repeated applications of the SHA256 compression function. + * + * This API exists to support environments that wish to route the + * SHA256 compression step through a hardware-accelerated or otherwise + * specialized implementation. It is NOT meant for replacing SHA256 + * with a different hash function. + * + * Args: ctx: pointer to a context object. + * In: fn_compression: pointer to a function implementing the compression function; + * passing NULL restores the default implementation. + */ +SECP256K1_API void secp256k1_context_set_sha256_compression( + secp256k1_context *ctx, + secp256k1_sha256_compression_function fn_compression +) SECP256K1_ARG_NONNULL(1); + +/** Parse a variable-length public key into the pubkey object. + * + * Returns: 1 if the public key was fully valid. + * 0 if the public key could not be parsed or is invalid. + * Args: ctx: pointer to a context object. + * Out: pubkey: pointer to a pubkey object. If 1 is returned, it is set to a + * parsed version of input. If not, its value is undefined. + * In: input: pointer to a serialized public key + * inputlen: length of the array pointed to by input + * + * This function supports parsing compressed (33 bytes, header byte 0x02 or + * 0x03), uncompressed (65 bytes, header byte 0x04), or hybrid (65 bytes, header + * byte 0x06 or 0x07) format public keys. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_parse( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *input, + size_t inputlen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a pubkey object into a serialized byte sequence. + * + * Returns: 1 always. + * Args: ctx: pointer to a context object. + * Out: output: pointer to a 65-byte (if compressed==0) or 33-byte (if + * compressed==1) byte array to place the serialized key + * in. + * In/Out: outputlen: pointer to an integer which is initially set to the + * size of output, and is overwritten with the written + * size. + * In: pubkey: pointer to a secp256k1_pubkey containing an + * initialized public key. + * flags: SECP256K1_EC_COMPRESSED if serialization should be in + * compressed format, otherwise SECP256K1_EC_UNCOMPRESSED. + */ +SECP256K1_API int secp256k1_ec_pubkey_serialize( + const secp256k1_context *ctx, + unsigned char *output, + size_t *outputlen, + const secp256k1_pubkey *pubkey, + unsigned int flags +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Compare two public keys using lexicographic (of compressed serialization) order + * + * Returns: <0 if the first public key is less than the second + * >0 if the first public key is greater than the second + * 0 if the two public keys are equal + * Args: ctx: pointer to a context object + * In: pubkey1: first public key to compare + * pubkey2: second public key to compare + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_cmp( + const secp256k1_context *ctx, + const secp256k1_pubkey *pubkey1, + const secp256k1_pubkey *pubkey2 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Sort public keys using lexicographic (of compressed serialization) order + * + * Returns: 0 if the arguments are invalid. 1 otherwise. + * + * Args: ctx: pointer to a context object + * In: pubkeys: array of pointers to pubkeys to sort + * n_pubkeys: number of elements in the pubkeys array + */ +SECP256K1_API int secp256k1_ec_pubkey_sort( + const secp256k1_context *ctx, + const secp256k1_pubkey **pubkeys, + size_t n_pubkeys +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2); + +/** Parse an ECDSA signature in compact (64 bytes) format. + * + * Returns: 1 when the signature could be parsed, 0 otherwise. + * Args: ctx: pointer to a context object + * Out: sig: pointer to a signature object + * In: input64: pointer to the 64-byte array to parse + * + * The signature must consist of a 32-byte big endian R value, followed by a + * 32-byte big endian S value. If R or S fall outside of [0..order-1], the + * encoding is invalid. R and S with value 0 are allowed in the encoding. + * + * After the call, sig will always be initialized. If parsing failed or R or + * S are zero, the resulting sig value is guaranteed to fail verification for + * any message and public key. + */ +SECP256K1_API int secp256k1_ecdsa_signature_parse_compact( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sig, + const unsigned char *input64 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Parse a DER ECDSA signature. + * + * Returns: 1 when the signature could be parsed, 0 otherwise. + * Args: ctx: pointer to a context object + * Out: sig: pointer to a signature object + * In: input: pointer to the signature to be parsed + * inputlen: the length of the array pointed to be input + * + * This function will accept any valid DER encoded signature, even if the + * encoded numbers are out of range. + * + * After the call, sig will always be initialized. If parsing failed or the + * encoded numbers are out of range, signature verification with it is + * guaranteed to fail for every message and public key. + */ +SECP256K1_API int secp256k1_ecdsa_signature_parse_der( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sig, + const unsigned char *input, + size_t inputlen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize an ECDSA signature in DER format. + * + * Returns: 1 if enough space was available to serialize, 0 otherwise + * Args: ctx: pointer to a context object + * Out: output: pointer to an array to store the DER serialization + * In/Out: outputlen: pointer to a length integer. Initially, this integer + * should be set to the length of output. After the call + * it will be set to the length of the serialization (even + * if 0 was returned). + * In: sig: pointer to an initialized signature object + */ +SECP256K1_API int secp256k1_ecdsa_signature_serialize_der( + const secp256k1_context *ctx, + unsigned char *output, + size_t *outputlen, + const secp256k1_ecdsa_signature *sig +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Serialize an ECDSA signature in compact (64 byte) format. + * + * Returns: 1 + * Args: ctx: pointer to a context object + * Out: output64: pointer to a 64-byte array to store the compact serialization + * In: sig: pointer to an initialized signature object + * + * See secp256k1_ecdsa_signature_parse_compact for details about the encoding. + */ +SECP256K1_API int secp256k1_ecdsa_signature_serialize_compact( + const secp256k1_context *ctx, + unsigned char *output64, + const secp256k1_ecdsa_signature *sig +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Verify an ECDSA signature. + * + * Returns: 1: correct signature + * 0: incorrect or unparseable signature + * Args: ctx: pointer to a context object + * In: sig: the signature being verified. + * msghash32: the 32-byte message hash being verified. + * The verifier must make sure to apply a cryptographic + * hash function to the message by itself and not accept an + * msghash32 value directly. Otherwise, it would be easy to + * create a "valid" signature without knowledge of the + * secret key. See also + * https://bitcoin.stackexchange.com/a/81116/35586 for more + * background on this topic. + * pubkey: pointer to an initialized public key to verify with. + * + * To avoid accepting malleable signatures, only ECDSA signatures in lower-S + * form are accepted. + * + * If you need to accept ECDSA signatures from sources that do not obey this + * rule, apply secp256k1_ecdsa_signature_normalize to the signature prior to + * verification, but be aware that doing so results in malleable signatures. + * + * For details, see the comments for that function. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ecdsa_verify( + const secp256k1_context *ctx, + const secp256k1_ecdsa_signature *sig, + const unsigned char *msghash32, + const secp256k1_pubkey *pubkey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Convert a signature to a normalized lower-S form. + * + * Returns: 1 if sigin was not normalized, 0 if it already was. + * Args: ctx: pointer to a context object + * Out: sigout: pointer to a signature to fill with the normalized form, + * or copy if the input was already normalized. (can be NULL if + * you're only interested in whether the input was already + * normalized). + * In: sigin: pointer to a signature to check/normalize (can be identical to sigout) + * + * With ECDSA a third-party can forge a second distinct signature of the same + * message, given a single initial signature, but without knowing the key. This + * is done by negating the S value modulo the order of the curve, 'flipping' + * the sign of the random point R which is not included in the signature. + * + * Forgery of the same message isn't universally problematic, but in systems + * where message malleability or uniqueness of signatures is important this can + * cause issues. This forgery can be blocked by all verifiers forcing signers + * to use a normalized form. + * + * The lower-S form reduces the size of signatures slightly on average when + * variable length encodings (such as DER) are used and is cheap to verify, + * making it a good choice. Security of always using lower-S is assured because + * anyone can trivially modify a signature after the fact to enforce this + * property anyway. + * + * The lower S value is always between 0x1 and + * 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, + * inclusive. + * + * No other forms of ECDSA malleability are known and none seem likely, but + * there is no formal proof that ECDSA, even with this additional restriction, + * is free of other malleability. Commonly used serialization schemes will also + * accept various non-unique encodings, so care should be taken when this + * property is required for an application. + * + * The secp256k1_ecdsa_sign function will by default create signatures in the + * lower-S form, and secp256k1_ecdsa_verify will not accept others. In case + * signatures come from a system that cannot enforce this property, + * secp256k1_ecdsa_signature_normalize must be called before verification. + */ +SECP256K1_API int secp256k1_ecdsa_signature_normalize( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sigout, + const secp256k1_ecdsa_signature *sigin +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(3); + +/** An implementation of RFC6979 (using HMAC-SHA256) as nonce generation function. + * If a data pointer is passed, it is assumed to be a pointer to 32 bytes of + * extra entropy. + */ +SECP256K1_API const secp256k1_nonce_function secp256k1_nonce_function_rfc6979; + +/** A default safe nonce generation function (currently equal to secp256k1_nonce_function_rfc6979). */ +SECP256K1_API const secp256k1_nonce_function secp256k1_nonce_function_default; + +/** Create an ECDSA signature. + * + * Returns: 1: signature created + * 0: the nonce generation function failed, or the secret key was invalid. + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * Out: sig: pointer to an array where the signature will be placed. + * In: msghash32: the 32-byte message hash being signed. + * seckey: pointer to a 32-byte secret key. + * noncefp: pointer to a nonce generation function. If NULL, + * secp256k1_nonce_function_default is used. + * ndata: pointer to arbitrary data used by the nonce generation function + * (can be NULL). If it is non-NULL and + * secp256k1_nonce_function_default is used, then ndata must be a + * pointer to 32-bytes of additional data. + * + * The created signature is always in lower-S form. See + * secp256k1_ecdsa_signature_normalize for more details. + */ +SECP256K1_API int secp256k1_ecdsa_sign( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sig, + const unsigned char *msghash32, + const unsigned char *seckey, + secp256k1_nonce_function noncefp, + const void *ndata +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Verify an elliptic curve secret key. + * + * A secret key is valid if it is not 0 and less than the secp256k1 curve order + * when interpreted as an integer (most significant byte first). The + * probability of choosing a 32-byte string uniformly at random which is an + * invalid secret key is negligible. However, if it does happen it should + * be assumed that the randomness source is severely broken and there should + * be no retry. + * + * Returns: 1: secret key is valid + * 0: secret key is invalid + * Args: ctx: pointer to a context object. + * In: seckey: pointer to a 32-byte secret key. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_verify( + const secp256k1_context *ctx, + const unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2); + +/** Compute the public key for a secret key. + * + * Returns: 1: secret was valid, public key stores. + * 0: secret was invalid, try again. + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * Out: pubkey: pointer to the created public key. + * In: seckey: pointer to a 32-byte secret key. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_create( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Negates a secret key in place. + * + * Returns: 0 if the given secret key is invalid according to + * secp256k1_ec_seckey_verify. 1 otherwise + * Args: ctx: pointer to a context object + * In/Out: seckey: pointer to the 32-byte secret key to be negated. If the + * secret key is invalid according to + * secp256k1_ec_seckey_verify, this function returns 0 and + * seckey will be set to some unspecified value. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_negate( + const secp256k1_context *ctx, + unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2); + +/** Negates a public key in place. + * + * Returns: 1 always + * Args: ctx: pointer to a context object + * In/Out: pubkey: pointer to the public key to be negated. + */ +SECP256K1_API int secp256k1_ec_pubkey_negate( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2); + +/** Tweak a secret key by adding tweak to it. + * + * Returns: 0 if the arguments are invalid or the resulting secret key would be + * invalid (only when the tweak is the negation of the secret key). 1 + * otherwise. + * Args: ctx: pointer to a context object. + * In/Out: seckey: pointer to a 32-byte secret key. If the secret key is + * invalid according to secp256k1_ec_seckey_verify, this + * function returns 0. seckey will be set to some unspecified + * value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak, which must be valid according to + * secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly + * random 32-byte tweaks, the chance of being invalid is + * negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_tweak_add( + const secp256k1_context *ctx, + unsigned char *seckey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Tweak a public key by adding tweak times the generator to it. + * + * Returns: 0 if the arguments are invalid or the resulting public key would be + * invalid (only when the tweak is the negation of the corresponding + * secret key). 1 otherwise. + * Args: ctx: pointer to a context object. + * In/Out: pubkey: pointer to a public key object. pubkey will be set to an + * invalid value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak, which must be valid according to + * secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly + * random 32-byte tweaks, the chance of being invalid is + * negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_tweak_add( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Tweak a secret key by multiplying it by a tweak. + * + * Returns: 0 if the arguments are invalid. 1 otherwise. + * Args: ctx: pointer to a context object. + * In/Out: seckey: pointer to a 32-byte secret key. If the secret key is + * invalid according to secp256k1_ec_seckey_verify, this + * function returns 0. seckey will be set to some unspecified + * value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to + * secp256k1_ec_seckey_verify, this function returns 0. For + * uniformly random 32-byte arrays the chance of being invalid + * is negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_tweak_mul( + const secp256k1_context *ctx, + unsigned char *seckey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Tweak a public key by multiplying it by a tweak value. + * + * Returns: 0 if the arguments are invalid. 1 otherwise. + * Args: ctx: pointer to a context object. + * In/Out: pubkey: pointer to a public key object. pubkey will be set to an + * invalid value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to + * secp256k1_ec_seckey_verify, this function returns 0. For + * uniformly random 32-byte arrays the chance of being invalid + * is negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_tweak_mul( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Randomizes the context to provide enhanced protection against side-channel leakage. + * + * Returns: 1: randomization successful + * 0: error + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * In: seed32: pointer to a 32-byte random seed (NULL resets to initial state). + * + * While secp256k1 code is written and tested to be constant-time no matter what + * secret values are, it is possible that a compiler may output code which is not, + * and also that the CPU may not emit the same radio frequencies or draw the same + * amount of power for all values. Randomization of the context shields against + * side-channel observations which aim to exploit secret-dependent behaviour in + * certain computations which involve secret keys. + * + * It is highly recommended to call this function on contexts returned from + * secp256k1_context_create or secp256k1_context_clone (or from the corresponding + * functions in secp256k1_preallocated.h) before using these contexts to call API + * functions that perform computations involving secret keys, e.g., signing and + * public key generation. It is possible to call this function more than once on + * the same context, and doing so before every few computations involving secret + * keys is recommended as a defense-in-depth measure. Randomization of the static + * context secp256k1_context_static is not supported. + * + * Currently, the random seed is mainly used for blinding multiplications of a + * secret scalar with the elliptic curve base point. Multiplications of this + * kind are performed by exactly those API functions which are documented to + * require a context that is not secp256k1_context_static. As a rule of thumb, + * these are all functions which take a secret key (or a keypair) as an input. + * A notable exception to that rule is the ECDH module, which relies on a different + * kind of elliptic curve point multiplication and thus does not benefit from + * enhanced protection against side-channel leakage currently. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_context_randomize( + secp256k1_context *ctx, + const unsigned char *seed32 +) SECP256K1_ARG_NONNULL(1); + +/** Add a number of public keys together. + * + * Returns: 1: the sum of the public keys is valid. + * 0: the sum of the public keys is not valid. + * Args: ctx: pointer to a context object. + * Out: out: pointer to a public key object for placing the resulting public key. + * In: ins: pointer to array of pointers to public keys. + * n: the number of public keys to add together (must be at least 1). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_combine( + const secp256k1_context *ctx, + secp256k1_pubkey *out, + const secp256k1_pubkey * const *ins, + size_t n +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Compute a tagged hash as defined in BIP-340. + * + * This is useful for creating a message hash and achieving domain separation + * through an application-specific tag. This function returns + * SHA256(SHA256(tag)||SHA256(tag)||msg). Therefore, tagged hash + * implementations optimized for a specific tag can precompute the SHA256 state + * after hashing the tag hashes. + * + * Returns: 1 always. + * Args: ctx: pointer to a context object + * Out: hash32: pointer to a 32-byte array to store the resulting hash + * In: tag: pointer to an array containing the tag + * taglen: length of the tag array + * msg: pointer to an array containing the message + * msglen: length of the message array + */ +SECP256K1_API int secp256k1_tagged_sha256( + const secp256k1_context *ctx, + unsigned char *hash32, + const unsigned char *tag, + size_t taglen, + const unsigned char *msg, + size_t msglen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_H */ diff --git a/internal/mpt-crypto-sys/vendor/include/secp256k1_mpt.h b/internal/mpt-crypto-sys/vendor/include/secp256k1_mpt.h new file mode 100644 index 00000000..256a9743 --- /dev/null +++ b/internal/mpt-crypto-sys/vendor/include/secp256k1_mpt.h @@ -0,0 +1,546 @@ +#ifndef SECP256K1_MPT_H +#define SECP256K1_MPT_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Generates a new secp256k1 key pair. + */ +SECP256K1_API int +secp256k1_elgamal_generate_keypair( + secp256k1_context const* ctx, + unsigned char* privkey, + secp256k1_pubkey* pubkey); + +/** + * @brief Encrypts a 64-bit amount using ElGamal. + */ +SECP256K1_API int +secp256k1_elgamal_encrypt( + secp256k1_context const* ctx, + secp256k1_pubkey* c1, + secp256k1_pubkey* c2, + secp256k1_pubkey const* pubkey_Q, + uint64_t amount, + unsigned char const* blinding_factor); + +/** + * @brief Decrypts an ElGamal ciphertext to recover the amount. + */ +SECP256K1_API int +secp256k1_elgamal_decrypt( + secp256k1_context const* ctx, + uint64_t* amount, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + unsigned char const* privkey); + +/** + * @brief Homomorphically adds two ElGamal ciphertexts. + */ +SECP256K1_API int +secp256k1_elgamal_add( + secp256k1_context const* ctx, + secp256k1_pubkey* sum_c1, + secp256k1_pubkey* sum_c2, + secp256k1_pubkey const* a_c1, + secp256k1_pubkey const* a_c2, + secp256k1_pubkey const* b_c1, + secp256k1_pubkey const* b_c2); + +/** + * @brief Homomorphically subtracts two ElGamal ciphertexts. + */ +SECP256K1_API int +secp256k1_elgamal_subtract( + secp256k1_context const* ctx, + secp256k1_pubkey* diff_c1, + secp256k1_pubkey* diff_c2, + secp256k1_pubkey const* a_c1, + secp256k1_pubkey const* a_c2, + secp256k1_pubkey const* b_c1, + secp256k1_pubkey const* b_c2); + +/** + * @brief Generates the canonical encrypted zero for a given MPT token instance. + * + * This ciphertext represents a zero balance for a specific account's holding + * of a token defined by its MPTokenIssuanceID. + * + * @param[in] ctx A pointer to a valid secp256k1 context. + * @param[out] enc_zero_c1 The C1 component of the canonical ciphertext. + * @param[out] enc_zero_c2 The C2 component of the canonical ciphertext. + * @param[in] pubkey The ElGamal public key of the account holder. + * @param[in] account_id A pointer to the 20-byte AccountID. + * @param[in] mpt_issuance_id A pointer to the 24-byte MPTokenIssuanceID. + * + * @return 1 on success, 0 on failure. + */ +SECP256K1_API int +generate_canonical_encrypted_zero( + secp256k1_context const* ctx, + secp256k1_pubkey* enc_zero_c1, + secp256k1_pubkey* enc_zero_c2, + secp256k1_pubkey const* pubkey, + unsigned char const* account_id, // 20 bytes + unsigned char const* mpt_issuance_id // 24 bytes +); + +// ... (includes and previous ElGamal declarations) ... + +/* +================================================================================ +| | +| PROOF OF KNOWLEDGE OF PLAINTEXT AND RANDOMNESS | +| (Chaum-Pedersen Equality Proof) | +================================================================================ +*/ + +/** + * @brief Generates a proof that an ElGamal ciphertext correctly encrypts a + * known plaintext `m` and that the prover knows the randomness `r`. + * + * @param[in] ctx A pointer to a valid secp256k1 context object, + * initialized for signing. + * @param[out] proof A pointer to a 98-byte buffer to store the proof + * (T1 [33 bytes] || T2 [33 bytes] || s [32 bytes]). + * @note Legacy uncompressed form; superseded by the compact proof APIs + * (secp256k1_compact_*). Removed in PR #22. + * @param[in] c1 The C1 component of the ciphertext (r*G). + * @param[in] c2 The C2 component of the ciphertext (m*G + r*Pk). + * @param[in] pk_recipient The public key used for encryption. + * @param[in] amount The known plaintext value `m`. + * @param[in] randomness_r The 32-byte secret random scalar `r` used in encryption. + * @param[in] tx_context_id A 32-byte unique identifier for the transaction context. + * + * @return 1 on success, 0 on failure. + */ +SECP256K1_API int +secp256k1_equality_plaintext_prove( + secp256k1_context const* ctx, + unsigned char* proof, // Output: 98 bytes + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk_recipient, + uint64_t amount, + unsigned char const* randomness_r, // Secret input + unsigned char const* tx_context_id // 32 bytes +); + +/** + * @brief Verifies a proof of knowledge of plaintext and randomness. + * + * Checks if the proof correctly demonstrates that (C1, C2) encrypts `m` + * under `pk_recipient`. + * + * @param[in] ctx A pointer to a valid secp256k1 context object, + * initialized for verification. + * @param[in] proof A pointer to the 98-byte proof to verify. + * @param[in] c1 The C1 component of the ciphertext. + * @param[in] c2 The C2 component of the ciphertext. + * @param[in] pk_recipient The public key used for encryption. + * @param[in] amount The known plaintext value `m`. + * @param[in] tx_context_id A 32-byte unique identifier for the transaction context. + * + * @return 1 if the proof is valid, 0 otherwise. + */ +SECP256K1_API int +secp256k1_equality_plaintext_verify( + secp256k1_context const* ctx, + unsigned char const* proof, // Input: 98 bytes + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk_recipient, + uint64_t amount, + unsigned char const* tx_context_id // 32 bytes +); + +// ... (rest of header, #endif etc.) + +/** + * @brief Computes a Pedersen Commitment: C = value*G + blinding_factor*Pk_base. + * + * This function creates the commitment point (C) that the Bulletproof proves + * the range of. Pk_base is the dynamic secondary generator (H). + * + * @param[in] ctx A pointer to the context. + * @param[out] commitment_C The resulting commitment point C. + * @param[in] value The secret amount v (uint64_t). + * @param[in] blinding_factor The secret randomness r (32 bytes). + * @param[in] pk_base The recipient's public key (used as the H generator). + * + * @return 1 on success, 0 on failure. + */ +SECP256K1_API int +secp256k1_bulletproof_create_commitment( + secp256k1_context const* ctx, + secp256k1_pubkey* commitment_C, + uint64_t value, + unsigned char const* blinding_factor, + secp256k1_pubkey const* pk_base); + +int +secp256k1_bulletproof_prove( + secp256k1_context const* ctx, + unsigned char* proof_out, + size_t* proof_len, + uint64_t value, + unsigned char const* blinding_factor, + secp256k1_pubkey const* pk_base, + unsigned char const* context_id, /* <--- AND HERE */ + unsigned int proof_type); + +int +secp256k1_bulletproof_verify( + secp256k1_context const* ctx, + secp256k1_pubkey const* G_vec, + secp256k1_pubkey const* H_vec, + unsigned char const* proof, + size_t proof_len, + secp256k1_pubkey const* commitment_C, + secp256k1_pubkey const* pk_base, /* This is generator H */ + unsigned char const* context_id); +/** + * @brief Proves the link between an ElGamal ciphertext and a Pedersen commitment. + * * Formal Statement: Knowledge of (m, r, rho) such that: + * C1 = r*G, C2 = m*G + r*Pk, and PCm = m*G + rho*H. + * * @param ctx Pointer to a secp256k1 context object. + * @param proof [OUT] Pointer to 195-byte buffer for the proof output. + * Legacy Variant B format; superseded by compact proof APIs. + * Removed in PR #22. + * @param c1 Pointer to the ElGamal C1 point (r*G). + * @param c2 Pointer to the ElGamal C2 point (m*G + r*Pk). + * @param pk Pointer to the recipient's public key. + * @param pcm Pointer to the Pedersen Commitment (m*G + rho*H). + * @param amount The plaintext amount (m). + * @param r The 32-byte secret ElGamal blinding factor. + * @param rho The 32-byte secret Pedersen blinding factor. + * @param context_id 32-byte unique transaction context identifier. + * @return 1 on success, 0 on failure. + */ +int +secp256k1_elgamal_pedersen_link_prove( + secp256k1_context const* ctx, + unsigned char* proof, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk, + secp256k1_pubkey const* pcm, + uint64_t amount, + unsigned char const* r, + unsigned char const* rho, + unsigned char const* context_id); + +/** + * @brief Verifies the link proof between ElGamal and Pedersen commitments. + * * @return 1 if the proof is valid, 0 otherwise. + */ +int +secp256k1_elgamal_pedersen_link_verify( + secp256k1_context const* ctx, + unsigned char const* proof, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk, + secp256k1_pubkey const* pcm, + unsigned char const* context_id); + +/** + * Verifies that (c1, c2) is a valid ElGamal encryption of 'amount' + * for 'pubkey_Q' using the revealed 'blinding_factor'. + */ +int +secp256k1_elgamal_verify_encryption( + secp256k1_context const* ctx, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pubkey_Q, + uint64_t amount, + unsigned char const* blinding_factor); + +/** Proof of Knowledge of Secret Key for Registration. + * Compact form: (e, s) in Z_q^2 = 64 bytes. + * Domain: "CMPT_POK_SK_REGISTER" */ +#define SECP256K1_POK_SK_PROOF_SIZE 64 + +SECP256K1_API int +secp256k1_mpt_pok_sk_prove( + secp256k1_context const* ctx, + unsigned char* proof, /* Expected size: 64 bytes */ + secp256k1_pubkey const* pk, + unsigned char const* sk, + unsigned char const* context_id); + +SECP256K1_API int +secp256k1_mpt_pok_sk_verify( + secp256k1_context const* ctx, + unsigned char const* proof, /* Expected size: 64 bytes */ + secp256k1_pubkey const* pk, + unsigned char const* context_id); + +/** + * Compute a Pedersen Commitment: PC = m*G + rho*H + * Returns 1 on success, 0 on failure. + */ +int +secp256k1_mpt_pedersen_commit( + secp256k1_context const* ctx, + secp256k1_pubkey* commitment, + uint64_t amount, + unsigned char const* blinding_factor_rho /* 32 bytes */ +); + +/** Get the standardized H generator for Pedersen Commitments */ +int +secp256k1_mpt_get_h_generator(secp256k1_context const* ctx, secp256k1_pubkey* h); + +/** + * @brief Generates a vector of N independent NUMS generators. + */ +int +secp256k1_mpt_get_generator_vector( + secp256k1_context const* ctx, + secp256k1_pubkey* vec, + size_t n, + unsigned char const* label, + size_t label_len); + +void +secp256k1_mpt_scalar_add(unsigned char* res, unsigned char const* a, unsigned char const* b); +void +secp256k1_mpt_scalar_mul(unsigned char* res, unsigned char const* a, unsigned char const* b); +void +secp256k1_mpt_scalar_inverse(unsigned char* res, unsigned char const* in); +void +secp256k1_mpt_scalar_negate(unsigned char* res, unsigned char const* in); +void +secp256k1_mpt_scalar_reduce32(unsigned char out32[32], unsigned char const in32[32]); + +/** + * Returns the size of the serialized proof for N recipients. + * Size: (1 + N) * 33 bytes for points + 2 * 32 bytes for scalars. + */ +size_t +secp256k1_mpt_proof_equality_shared_r_size(size_t n); + +/** + * Generates a proof that multiple ciphertexts encrypt the same amount m + * using the SAME shared randomness r. + */ +int +secp256k1_mpt_prove_equality_shared_r( + secp256k1_context const* ctx, + unsigned char* proof_out, + uint64_t amount, + unsigned char const* r_shared, + size_t n, + secp256k1_pubkey const* C1, + secp256k1_pubkey const* C2_vec, + secp256k1_pubkey const* Pk_vec, + unsigned char const* context_id); + +/** + * Verifies the proof of equality with shared randomness. + */ +int +secp256k1_mpt_verify_equality_shared_r( + secp256k1_context const* ctx, + unsigned char const* proof, + size_t n, + secp256k1_pubkey const* C1, + secp256k1_pubkey const* C2_vec, + secp256k1_pubkey const* Pk_vec, + unsigned char const* context_id); + +int +secp256k1_bulletproof_prove_agg( + secp256k1_context const* ctx, + unsigned char* proof_out, + size_t* proof_len, + uint64_t const* values, + unsigned char const* blindings_flat, + size_t m, + secp256k1_pubkey const* pk_base, + unsigned char const* context_id); +int +secp256k1_bulletproof_verify_agg( + secp256k1_context const* ctx, + secp256k1_pubkey const* G_vec, /* length n = 64*m */ + secp256k1_pubkey const* H_vec, /* length n = 64*m */ + unsigned char const* proof, + size_t proof_len, + secp256k1_pubkey const* commitment_C_vec, /* length m */ + size_t m, + secp256k1_pubkey const* pk_base, + unsigned char const* context_id); + +/* +================================================================================ +| | +| AND-COMPOSED COMPACT SIGMA PROOF (STANDARD EG) | +| | +================================================================================ + * + * Combines ciphertext equality, Pedersen linkage, and balance verification + * into a single sigma protocol under a shared Fiat-Shamir challenge. + * + * Language: exists (r, m, sk_A, rho, b) in Z_q^5 such that: + * C1 = r*G + * C_{2,i} = m*G + r*pk_i for i = 1..n + * PC_m = m*G + r*H + * pk_A = sk_A*G + * PC_b = b*G + rho*H + * B2 - b*G = sk_A*B1 + * + * Compact proof: (e, z_m, z_r, z_b, z_rho, z_sk) in Z_q^6 = 192 bytes. + * Fiat-Shamir domain: "CMPT_SEND_SIGMA" + */ + +/** Serialized size of the compact standard proof in bytes. */ +#define SECP256K1_COMPACT_STANDARD_PROOF_SIZE 192 + +/** + * @brief Generate a compact AND-composed sigma proof for standard EC-ElGamal. + * + * proof_out must point to a buffer of SECP256K1_COMPACT_STANDARD_PROOF_SIZE + * bytes. context_id is an optional 32-byte transaction context (may be NULL). + */ +SECP256K1_API int +secp256k1_compact_standard_prove( + secp256k1_context const* ctx, + unsigned char* proof_out, + uint64_t amount, + uint64_t balance, + unsigned char const* r_shared, + unsigned char const* sk_A, + unsigned char const* r_b, + size_t n, + secp256k1_pubkey const* C1, + secp256k1_pubkey const* C2_vec, + secp256k1_pubkey const* Pk_vec, + secp256k1_pubkey const* PC_m, + secp256k1_pubkey const* pk_A, + secp256k1_pubkey const* PC_b, + secp256k1_pubkey const* B1, + secp256k1_pubkey const* B2, + unsigned char const* context_id); + +/** + * @brief Verify a compact AND-composed sigma proof for standard EC-ElGamal. + * + * Returns 1 if the proof is valid, 0 otherwise. + */ +SECP256K1_API int +secp256k1_compact_standard_verify( + secp256k1_context const* ctx, + unsigned char const* proof, + size_t n, + secp256k1_pubkey const* C1, + secp256k1_pubkey const* C2_vec, + secp256k1_pubkey const* Pk_vec, + secp256k1_pubkey const* PC_m, + secp256k1_pubkey const* pk_A, + secp256k1_pubkey const* PC_b, + secp256k1_pubkey const* B1, + secp256k1_pubkey const* B2, + unsigned char const* context_id); + +/* +================================================================================ +| | +| COMPACT SIGMA PROOF — CLAWBACK | +| | +================================================================================ + * + * Proves the issuer knows sk_iss consistent with the on-ledger mirror + * ciphertext (C1, C2) and the publicly declared amount m: + * P_iss = sk_iss * G + * C2 - m*G = sk_iss * C1 + * + * Compact proof: (e, z_sk) in Z_q^2 = 64 bytes. + * Fiat-Shamir domain: "CMPT_CLAWBACK_SIGMA" + */ + +#define SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE 64 + +SECP256K1_API int +secp256k1_compact_clawback_prove( + secp256k1_context const* ctx, + unsigned char* proof_out, + uint64_t amount, + unsigned char const* sk_iss, + secp256k1_pubkey const* P_iss, + secp256k1_pubkey const* C1, + secp256k1_pubkey const* C2, + unsigned char const* context_id); + +SECP256K1_API int +secp256k1_compact_clawback_verify( + secp256k1_context const* ctx, + unsigned char const* proof, + uint64_t amount, + secp256k1_pubkey const* P_iss, + secp256k1_pubkey const* C1, + secp256k1_pubkey const* C2, + unsigned char const* context_id); + +/* +================================================================================ +| | +| COMPACT SIGMA PROOF — CONVERTBACK | +| | +================================================================================ + * + * AND-composed proof for balance linkage in a ConvertBack withdrawal. + * The withdrawal ciphertext (C1_w, C2_w) is verified deterministically + * using the publicly disclosed r_w (BlindingFactor field), so the sigma + * proof covers only key ownership, balance decryption, and commitment. + * + * Language: exists (b, sk_A, rho) in Z_q^3 such that: + * P_A = sk_A*G + * B2 - b*G = sk_A*B1 + * PC_b = b*G + rho*H + * + * Compact proof: (e, z_b, z_rho, z_sk) in Z_q^4 = 128 bytes. + * Fiat-Shamir domain: "CMPT_CONVERTBACK_SIGMA" + * + * The caller must separately verify the withdrawal ciphertext: + * C1_w == r_w*G and C2_w == m*G + r_w*P_A + * using secp256k1_elgamal_verify_encryption() or equivalent. + */ + +#define SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE 128 + +SECP256K1_API int +secp256k1_compact_convertback_prove( + secp256k1_context const* ctx, + unsigned char* proof_out, + uint64_t balance, + unsigned char const* sk_A, + unsigned char const* rho, + secp256k1_pubkey const* pk_A, + secp256k1_pubkey const* B1, + secp256k1_pubkey const* B2, + secp256k1_pubkey const* PC_b, + unsigned char const* context_id); + +SECP256K1_API int +secp256k1_compact_convertback_verify( + secp256k1_context const* ctx, + unsigned char const* proof, + secp256k1_pubkey const* pk_A, + secp256k1_pubkey const* B1, + secp256k1_pubkey const* B2, + secp256k1_pubkey const* PC_b, + unsigned char const* context_id); + +#ifdef __cplusplus +} +#endif + +#endif // SECP256K1_MPT_H diff --git a/internal/mpt-crypto-sys/vendor/include/utility/mpt_utility.h b/internal/mpt-crypto-sys/vendor/include/utility/mpt_utility.h new file mode 100644 index 00000000..2416c283 --- /dev/null +++ b/internal/mpt-crypto-sys/vendor/include/utility/mpt_utility.h @@ -0,0 +1,509 @@ +#ifndef MPT_UTILITY_H +#define MPT_UTILITY_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// XRPL Transaction Types, the number MUST match rippled's definitions +#define ttCONFIDENTIAL_MPT_CONVERT 85 +#define ttCONFIDENTIAL_MPT_MERGE_INBOX 86 +#define ttCONFIDENTIAL_MPT_CONVERT_BACK 87 +#define ttCONFIDENTIAL_MPT_SEND 88 +#define ttCONFIDENTIAL_MPT_CLAWBACK 89 + +// General crypto primitive sizes in bytes +#define kMPT_HALF_SHA_SIZE 32 +#define kMPT_PUBKEY_SIZE 33 +#define kMPT_PRIVKEY_SIZE 32 +#define kMPT_BLINDING_FACTOR_SIZE 32 + +// Gamal & Pedersen primitive sizes in bytes +#define kMPT_ELGAMAL_CIPHER_SIZE 33 +#define kMPT_ELGAMAL_TOTAL_SIZE 66 +#define kMPT_PEDERSEN_COMMIT_SIZE 33 + +// Proof sizes in bytes +#define kMPT_SCHNORR_PROOF_SIZE 64 +#define kMPT_SINGLE_BULLETPROOF_SIZE 688 +#define kMPT_DOUBLE_BULLETPROOF_SIZE 754 + +// Context hash size +#define kMPT_ZKP_CONTEXT_HASH_SIZE 74 + +// Account ID size in bytes +#define kMPT_ACCOUNT_ID_SIZE 20 + +// MPTokenIssuance ID size in bytes +#define kMPT_ISSUANCE_ID_SIZE 24 + +/** + * @brief Represents a unique 24-byte MPT issuance ID. + */ +typedef struct +{ + uint8_t bytes[kMPT_ISSUANCE_ID_SIZE]; +} mpt_issuance_id; + +/** + * @brief Represents a 20-byte account ID. + * + * - bytes: Raw 20-byte array containing the AccountID. + */ +typedef struct account_id +{ + uint8_t bytes[kMPT_ACCOUNT_ID_SIZE]; +} account_id; + +/** + * @brief Represents a participant in a Confidential Send transaction. + * + * - pubkey: The 33-byte compressed secp256k1 public key. + * - ciphertext: The 66-byte ElGamal encrypted amount. + */ +typedef struct mpt_confidential_participant +{ + uint8_t pubkey[kMPT_PUBKEY_SIZE]; + uint8_t ciphertext[kMPT_ELGAMAL_TOTAL_SIZE]; +} mpt_confidential_participant; + +/** + * @brief Parameters required to generate a Pedersen Linkage Proof. + * + * - pedersen_commitment: The 64-byte Pedersen commitment. + * - amount: The actual numeric value being committed. + * - ciphertext: The 66-byte buffer containing the encrypted amount. + * - blinding_factor: The 32-byte secret value used to blind the commitment. + */ +typedef struct mpt_pedersen_proof_params +{ + uint8_t pedersen_commitment[kMPT_PEDERSEN_COMMIT_SIZE]; + uint64_t amount; + uint8_t ciphertext[kMPT_ELGAMAL_TOTAL_SIZE]; + uint8_t blinding_factor[kMPT_BLINDING_FACTOR_SIZE]; +} mpt_pedersen_proof_params; + +/** + * @brief Returns a globally shared secp256k1 context. + */ +secp256k1_context* +mpt_secp256k1_context(); + +/** + * @brief Context Hash for ConfidentialMPTConvert. + */ +int +mpt_get_convert_context_hash( + account_id account, + mpt_issuance_id iss, + uint32_t sequence, + uint8_t out_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Context Hash for ConfidentialMPTConvertBack. + */ +int +mpt_get_convert_back_context_hash( + account_id acc, + mpt_issuance_id iss, + uint32_t seq, + uint32_t ver, + uint8_t out_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Context Hash for ConfidentialMPTSend. + */ +int +mpt_get_send_context_hash( + account_id acc, + mpt_issuance_id iss, + uint32_t seq, + account_id dest, + uint32_t ver, + uint8_t out_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Context Hash for ConfidentialMPTClawback. + */ +int +mpt_get_clawback_context_hash( + account_id acc, + mpt_issuance_id iss, + uint32_t seq, + account_id holder, + uint8_t out_hash[kMPT_HALF_SHA_SIZE]); + +/* ============================================================================ + * Key & Ciphertext Utilities + * ============================================================================ */ + +/** + * @brief Parses a 66-byte buffer into two internal secp256k1 public keys. + * @param buffer [in] 66-byte buffer containing two points. + * @param out1 [out] First internal public key (C1). + * @param out2 [out] Second internal public key (C2). + * @return true on success, false if parsing fails. + */ +bool +mpt_make_ec_pair( + uint8_t const buffer[kMPT_ELGAMAL_TOTAL_SIZE], + secp256k1_pubkey* out1, + secp256k1_pubkey* out2); + +/** + * @brief Serializes two internal secp256k1 public keys into a 66-byte buffer. + * @param in1 [in] Internal format of the first point (C1). + * @param in2 [in] Internal format of the second point (C2). + * @param out [out] 66-byte buffer to write the serialized points. + * @return true if both points were valid and successfully serialized, false otherwise. + */ +bool +mpt_serialize_ec_pair( + secp256k1_pubkey const* in1, + secp256k1_pubkey const* in2, + uint8_t out[kMPT_ELGAMAL_TOTAL_SIZE]); + +/** + * @brief Generates a new Secp256k1 ElGamal keypair. + * @param out_privkey [out] A 32-byte buffer for private key. + * @param out_pubkey [out] A 33-byte buffer for public key. + * @return 0 on success, -1 on failure. + */ +int +mpt_generate_keypair(uint8_t* out_privkey, uint8_t* out_pubkey); + +/** + * @brief Generates a 32-byte blinding factor. + * @param out_factor [out] A 32-byte buffer to store the blinding factor. + * @return 0 on success, -1 on failure. + */ +int +mpt_generate_blinding_factor(uint8_t out_factor[kMPT_BLINDING_FACTOR_SIZE]); + +/** + * @brief Encrypts an uint64 amount using an ElGamal public key. + * @param amount [in] The integer value to encrypt. + * @param pubkey [in] The 33-byte public key. + * @param blinding_factor [in] The 32-byte random blinding factor (scalar r). + * @param out_ciphertext [out] A 66-byte buffer to store the resulting ciphertext (C1, C2). + * @return 0 on success, -1 on failure. + */ +int +mpt_encrypt_amount( + uint64_t amount, + uint8_t const pubkey[kMPT_PUBKEY_SIZE], + uint8_t const blinding_factor[kMPT_BLINDING_FACTOR_SIZE], + uint8_t out_ciphertext[kMPT_ELGAMAL_TOTAL_SIZE]); + +/** + * @brief Decrypts an MPT amount from a ciphertext pair. + * @param ciphertext [in] A 66-byte buffer containing the two points (C1, C2). + * @param privkey [in] The 32-byte private key. + * @param out_amount [out] Pointer to store the decrypted uint64_t amount. + * @return 0 on success, -1 on failure. + */ +int +mpt_decrypt_amount( + uint8_t const ciphertext[kMPT_ELGAMAL_TOTAL_SIZE], + uint8_t const privkey[kMPT_PRIVKEY_SIZE], + uint64_t* out_amount); + +/* ============================================================================ + * ZKProof Generation + * ============================================================================ */ + +/** + * @brief Generates a Schnorr Proof of Knowledge for a Confidential MPT conversion. + * + * This proof is used in 'ConfidentialMPTConvert' transactions to prove the + * sender possesses the private key associated with the account, binding it + * to the specific transaction via the ctx_hash. + * + * @param pubkey [in] 33-byte public key of the account. + * @param privkey [in] 32-byte private key of the account. + * @param ctx_hash [in] 32-byte hash of the transaction (challenge). + * @param out_proof [out] 64-byte buffer to store the compact Schnorr proof. + * @return 0 on success, -1 on failure. + */ +int +mpt_get_convert_proof( + uint8_t const pubkey[kMPT_PUBKEY_SIZE], + uint8_t const privkey[kMPT_PRIVKEY_SIZE], + uint8_t const ctx_hash[kMPT_HALF_SHA_SIZE], + uint8_t out_proof[kMPT_SCHNORR_PROOF_SIZE]); + +/** + * @brief Computes a Pedersen Commitment point for Confidential MPT. + * @param amount [in] The 64-bit unsigned integer value to commit. + * @param blinding_factor [in] A 32-byte secret scalar (rho) used to hide the amount. + * @param out_commitment [out] A 33-byte buffer to store the commitment + */ +int +mpt_get_pedersen_commitment( + uint64_t amount, + uint8_t const blinding_factor[kMPT_BLINDING_FACTOR_SIZE], + uint8_t out_commitment[kMPT_PEDERSEN_COMMIT_SIZE]); + +/** + * @brief Generates proof for ConfidentialMPTSend. + * + * Produces a compact AND-composed sigma proof (192 bytes) that simultaneously + * proves ciphertext equality, Pedersen commitment linkage, and balance ownership + * under a single Fiat-Shamir challenge, followed by an aggregated Bulletproof + * range proof (754 bytes). Total proof size is fixed at 946 bytes. + * + * pc_m must be computed as m*G + r*H using tx_blinding_factor as the blinding + * factor (not an independent scalar), since the compact sigma proof binds pc_m + * to the ciphertext randomness r. + * + * @param priv [in] The sender's 32-byte private key. + * @param pub [in] The sender's 33-byte public key. + * @param amount [in] The amount being sent. + * @param participants [in] List of participants, including Sender, Dest, Issuer, + * Auditor(optional). + * @param n_participants [in] Number of participants (3 or 4). + * @param tx_blinding_factor [in] The ElGamal randomness r (also blinding factor for pc_m). + * @param context_hash [in] The 32-byte context hash. + * @param amount_commitment [in] Pedersen commitment pc_m = m*G + r*H. + * @param balance_params [in] Includes pedersen_commitment (pc_b), amount (balance), + * blinding_factor (rho), and ciphertext (b1||b2). + * @param out_proof [out] Buffer to receive the proof blob. + * @param out_len [in/out] In: capacity (must be >= 946). Out: bytes written. + * @return 0 on success, -1 on failure. + */ +int +mpt_get_confidential_send_proof( + uint8_t const priv[kMPT_PRIVKEY_SIZE], + uint8_t const pub[kMPT_PUBKEY_SIZE], + uint64_t amount, + mpt_confidential_participant const* participants, + size_t n_participants, + uint8_t const tx_blinding_factor[kMPT_BLINDING_FACTOR_SIZE], + uint8_t const context_hash[kMPT_HALF_SHA_SIZE], + uint8_t const amount_commitment[kMPT_PEDERSEN_COMMIT_SIZE], + mpt_pedersen_proof_params const* balance_params, + uint8_t* out_proof, + size_t* out_len); + +/** + * @brief Generates proof for ConfidentialMPTConvertBack. + * + * Produces a compact AND-composed sigma proof (128 bytes) over the balance + * witness (b, rho, priv), followed by a single Bulletproof range proof (688 + * bytes) over the remainder commitment pc_rem = pc_b - m*G. + * Total proof size: 816 bytes (SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE + + * kMPT_SINGLE_BULLETPROOF_SIZE). + * + * @param priv [in] The holder's 32-byte private key. + * @param pub [in] The holder's 33-byte public key. + * @param context_hash [in] The 32-byte context hash binding the proof to the transaction. + * @param amount [in] The publicly revealed conversion amount m. + * @param params [in] Includes pedersen_commitment (pc_b), blinding_factor (rho), + * amount (balance b), and ciphertext (b1||b2). + * @param out_proof [out] 816-byte buffer for the compact sigma proof and range proof. + * @return 0 on success, -1 on failure. + */ +int +mpt_get_convert_back_proof( + uint8_t const priv[kMPT_PRIVKEY_SIZE], + uint8_t const pub[kMPT_PUBKEY_SIZE], + uint8_t const context_hash[kMPT_HALF_SHA_SIZE], + uint64_t const amount, + mpt_pedersen_proof_params const* params, + uint8_t out_proof[SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE + kMPT_SINGLE_BULLETPROOF_SIZE]); + +/** + * @brief Generates proof for ConfidentialMPTClawback. + * @param priv [in] The issuer's 32-byte private key. + * @param pub [in] The issuer's 33-byte compressed public key. + * @param context_hash [in] The 32-byte context hash binding the proof to the transaction. + * @param amount [in] The publicly known amount to be clawed back. + * @param ciphertext [in] The 66-byte sfIssuerEncryptedBalance blob from the ledger. + * @param out_proof [out] 64-byte buffer for the compact sigma proof. + * @return 0 on success, -1 on failure. + */ +int +mpt_get_clawback_proof( + uint8_t const priv[kMPT_PRIVKEY_SIZE], + uint8_t const pub[kMPT_PUBKEY_SIZE], + uint8_t const context_hash[kMPT_HALF_SHA_SIZE], + uint64_t const amount, + uint8_t const ciphertext[kMPT_ELGAMAL_TOTAL_SIZE], + uint8_t out_proof[SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE]); + +/* ============================================================================ + * Non-ZKP Validation + * ============================================================================ */ + +/** + * @brief Verifies that the ElGamal ciphertexts held by all participants encrypt + * the same revealed plaintext amount under the given blinding factor. + * + * @param amount [in] Actual numeric amount to verify against. + * @param blinding_factor [in] The ElGamal randomness r used to produce all ciphertexts. + * @param holder [in] Holder's public key and ciphertext. + * @param issuer [in] Issuer's public key and ciphertext. + * @param auditor [in] Auditor's public key and ciphertext, optional. + * @return 0 on success, -1 on failure. + */ +int +mpt_verify_revealed_amount( + uint64_t const amount, + uint8_t const blinding_factor[kMPT_BLINDING_FACTOR_SIZE], + mpt_confidential_participant const* holder, + mpt_confidential_participant const* issuer, + mpt_confidential_participant const* auditor); + +/* ============================================================================ + * ZKProof Verifications for Each Transaction + * ============================================================================ */ + +/** + * @brief Verify proof for ConfidentialMPTConvert. + * + * Proves that the sender possesses the private key for the provided public key. + * + * @param proof [in] The 64-byte compact Schnorr proof. + * @param pubkey [in] The 33-byte compressed ElGamal public key. + * @param context_hash [in] The 32-byte transaction context hash. + * @return 0 on success, -1 on failure. + */ +int +mpt_verify_convert_proof( + uint8_t const proof[kMPT_SCHNORR_PROOF_SIZE], + uint8_t const pubkey[kMPT_PUBKEY_SIZE], + uint8_t const context_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Verify proof for ConfidentialMPTConvertBack + * + * Proves that the hidden balance matches the commitment and that + * subtracting the transparent amount results in a non-negative balance. + * + * @param proof [in] 816-byte proof blob (compact sigma || Bulletproof). + * @param pubkey [in] The holder's 33-byte ElGamal public key. + * @param ciphertext [in] The holder's 66-byte balance ciphertext. + * @param balance_commitment [in] The 33-byte Pedersen commitment to the balance. + * @param amount [in] The publicly revealed conversion amount m. + * @param context_hash [in] The 32-byte transaction context hash. + * @return 0 on success, -1 on failure. + */ +int +mpt_verify_convert_back_proof( + uint8_t const proof[SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE + kMPT_SINGLE_BULLETPROOF_SIZE], + uint8_t const pubkey[kMPT_PUBKEY_SIZE], + uint8_t const ciphertext[kMPT_ELGAMAL_TOTAL_SIZE], + uint8_t const balance_commitment[kMPT_PEDERSEN_COMMIT_SIZE], + uint64_t const amount, + uint8_t const context_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Verify proof for ConfidentialMPTSend. + * + * Verifies the compact AND-composed sigma proof (first 192 bytes) that proves + * ciphertext correctness, Pedersen commitment linkage, and balance ownership, + * followed by an aggregated Bulletproof range proof (next 754 bytes). + * Proof size is fixed at 946 bytes. + * + * @param proof [in] 946-byte proof blob (compact sigma || Bulletproof). + * @param participants [in] List of participants' public keys and ciphertexts. + * participants[0] is the sender. + * @param n_participants [in] Number of participants (3 or 4). + * @param sender_spending_ciphertext [in] The sender's on-ledger balance ciphertext (b1||b2). + * @param amount_commitment [in] Pedersen commitment pc_m to the transfer amount. + * @param balance_commitment [in] Pedersen commitment pc_b to the sender's balance. + * @param context_hash [in] The 32-byte transaction context hash. + * @return 0 on success, -1 on failure. + */ +int +mpt_verify_send_proof( + uint8_t const* proof, + mpt_confidential_participant const* participants, + uint8_t const n_participants, + uint8_t const sender_spending_ciphertext[kMPT_ELGAMAL_TOTAL_SIZE], + uint8_t const amount_commitment[kMPT_PEDERSEN_COMMIT_SIZE], + uint8_t const balance_commitment[kMPT_PEDERSEN_COMMIT_SIZE], + uint8_t const context_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Verify proof for ConfidentialMPTClawback. + * + * Proves that a ciphertext, when decrypted by the issuer, results in exactly the plaintext amount + * specified in the transaction. + * + * @param proof [in] The 64-byte compact sigma proof. + * @param amount [in] The publicly known amount to be clawed back. + * @param pubkey [in] The issuer's 33-byte compressed public key. + * @param ciphertext [in] The 66-byte sfIssuerEncryptedBalance blob associated with the holder's + * account on the ledger. + * @param context_hash [in] The 32-byte transaction context hash. + * @return 0 on success, -1 on failure. + */ +int +mpt_verify_clawback_proof( + uint8_t const proof[SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE], + uint64_t const amount, + uint8_t const pubkey[kMPT_PUBKEY_SIZE], + uint8_t const ciphertext[kMPT_ELGAMAL_TOTAL_SIZE], + uint8_t const context_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Helper function to substract a transparent amount from a hidden commitment. + * + * @param commitment_in [in] The 33-byte starting Pedersen commitment. + * @param amount [in] The integer amount to subtract. + * @param commitment_out [out] The resulting 33-byte remainder commitment. + * @return 0 on success, -1 on failure. + */ +int +mpt_compute_convert_back_remainder( + uint8_t const commitment_in[kMPT_PEDERSEN_COMMIT_SIZE], + uint64_t amount, + uint8_t commitment_out[kMPT_PEDERSEN_COMMIT_SIZE]); + +/** + * @brief Generic verifier for aggregated Bulletproofs (Range Proofs). + * + * @param proof [in] The serialized Bulletproof buffer. + * @param proof_len [in] The length of the proof buffer in bytes. + * @param compressed_commitments [in] An array of pointers to the 33-byte Pedersen commitments. + * @param m [in] The number of commitments to verify. + * @param context_hash [in] The 32-byte context hash binding the proof to the transaction. + * @return 0 on success, -1 on failure. + */ +int +mpt_verify_aggregated_bulletproof( + uint8_t const* proof, + size_t proof_len, + uint8_t const** compressed_commitments, + size_t m, + uint8_t const context_hash[kMPT_HALF_SHA_SIZE]); + +/** + * @brief Verifies that the sending amount and remaining balance reside within the valid range from + * 0 to 2^64-1. + * + * @param ctx [in] secp256k1-zkp context. + * @param proof [in] 754-byte Double Bulletproof. + * @param amount_commitment [in] Pedersen commitment to the transfer amount. + * @param balance_commitment [in] Pedersen commitment to the sender's total balance. + * @param context_hash [in] 32-byte transaction context hash. + * @return 0 on success, -1 on failure. + */ +int +mpt_verify_send_range_proof( + uint8_t const proof[kMPT_DOUBLE_BULLETPROOF_SIZE], + uint8_t const amount_commitment[kMPT_PEDERSEN_COMMIT_SIZE], + uint8_t const balance_commitment[kMPT_PEDERSEN_COMMIT_SIZE], + uint8_t const context_hash[kMPT_HALF_SHA_SIZE]); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/internal/mpt-crypto/Cargo.toml b/internal/mpt-crypto/Cargo.toml new file mode 100644 index 00000000..29480756 --- /dev/null +++ b/internal/mpt-crypto/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "mpt-crypto" +version = "0.1.0" +edition = "2024" +description = "Safe Rust wrappers around mpt-crypto-sys (XLS-0096 Confidential MPTs)." +license = "ISC" +repository = "https://github.com/sephynox/xrpl-rust" +publish = false + +[lib] +name = "mpt_crypto" +# Doctests merged-binary loads libmpt-crypto.dylib at startup; the merged +# binary runs from a temp dir where our @loader_path-based rpath can't find +# it. End-to-end coverage lives in `tests/integration.rs` instead. +doctest = false + +[dependencies] +mpt-crypto-sys = { path = "../mpt-crypto-sys", version = "0.1.0" } +zeroize = { version = "1.8", features = ["derive"] } +thiserror = "2.0" + +[dev-dependencies] +# Tests reach into the sys crate directly to invoke C-side verifiers as oracles. +mpt-crypto-sys = { path = "../mpt-crypto-sys", version = "0.1.0" } diff --git a/internal/mpt-crypto/README.md b/internal/mpt-crypto/README.md new file mode 100644 index 00000000..8bb6e60b --- /dev/null +++ b/internal/mpt-crypto/README.md @@ -0,0 +1,225 @@ +# mpt-crypto + +Safe Rust wrappers around the [mpt-crypto](https://github.com/XRPLF/mpt-crypto) +C library, providing the cryptographic primitives needed to construct +[XLS-0096 Confidential Multi-Purpose Token](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0096-confidential-mpt) +transactions. + +This is the public-facing crate that xrpl-rust uses internally. It curates +the ~12 functions a client library actually needs from the ~88 raw FFI +bindings in `mpt-crypto-sys`, and adds idiomatic Rust types, `Result`-based +error handling, and `Zeroize`-on-drop secret material. + +## Architecture + +This crate sits in the middle of a four-layer stack: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ xrpl-rust transaction models │ +│ (ConfidentialMPTConvert / Send / ConvertBack / Clawback) │ +└────────────────────────────┬────────────────────────────────┘ + │ uses safe types and Result + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ mpt-crypto ◀──── this crate │ +│ - typed newtypes (Privkey, Pubkey, Ciphertext, …) │ +│ - Result-based error handling │ +│ - Zeroize-on-drop for secret material │ +│ - per-transaction-type ProofParams structs │ +└────────────────────────────┬────────────────────────────────┘ + │ unsafe extern "C" calls + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ mpt-crypto-sys (raw FFI, bindgen-generated) │ +│ - 88 `unsafe extern "C"` functions │ +│ - C-shaped types: `*const u8`, `*mut u8`, raw structs │ +└────────────────────────────┬────────────────────────────────┘ + │ dynamic linking via rpath + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ libmpt-crypto.{dylib,so,dll} │ +│ (cryptography in C, secp256k1 + OpenSSL statically inside) │ +└─────────────────────────────────────────────────────────────┘ +``` + +Each layer has one job: + +| Layer | Concern | +|---|---| +| Native library | Cryptographic algorithms | +| `mpt-crypto-sys` | Make the C symbols callable from Rust | +| **`mpt-crypto`** | **Make the FFI safe and idiomatic** | +| Transaction models | Map cryptographic primitives onto XRPL transaction shapes | + +## Module map + +| Module | Purpose | Key items | +|---|---|---| +| [`types`] | Strongly-typed byte-array newtypes | `Privkey`, `Pubkey`, `Ciphertext`, `Commitment`, `ContextHash`, `AccountId`, `IssuanceId`, `BlindingFactor`, the four `*Proof` types | +| [`error`] | Error enum + `Result` alias | `Error::NonZeroRc(i32)`, `Error::Invariant(&'static str)` | +| [`keypair`] | ElGamal/secp256k1 keypair generation | `keypair::generate() -> Result<(Privkey, Pubkey)>` | +| [`encrypt`] | EC-ElGamal encrypt / decrypt + RNG | `encrypt::{encrypt, decrypt, random_blinding_factor}` | +| [`commit`] | Pedersen commitments | `commit::pedersen(amount, blinding) -> Result`, `commit::convert_back_remainder` | +| [`context`] | Per-transaction context hashes (replay-binding) | `context::{convert, convert_back, send, clawback}` | +| [`prove`] | The four per-transaction proof generators | `prove::{convert, send, convert_back, clawback}` plus `Participant`, `SendProofParams`, `ConvertBackProofParams` | +| [`verify`] | Proof verifiers (the counterparts to `prove`) | `verify::{convert, send, convert_back, clawback, revealed_amount, send_range_proof, aggregated_bulletproof}` plus `SendVerifyParams`, `ConvertBackVerifyParams` | + +`lib.rs` re-exports `Error`, `Result`, and everything in `types`, so most +consumers can `use mpt_crypto::{Privkey, Pubkey, …}` directly without +naming `types`. + +## Type discipline + +Two flavors of newtype, distinguished by sensitivity: + +### Public-information types — built with the `public_bytes!` macro + +`Pubkey`, `Ciphertext`, `Commitment`, `ContextHash`, `AccountId`, +`IssuanceId`, and the four `*Proof` types. These are: + +- `#[derive(Clone, Copy, PartialEq, Eq, Hash)]` — cheap to pass around. +- `Debug` — abbreviated as `Pubkey(0xab12cd34…ef56gh78)` (first 4 + last 4 + bytes), to keep log output readable, **not** for security. + +### Secret types — built with the `secret_bytes!` macro + +`Privkey` and `BlindingFactor`. These are: + +- **Not `Copy`** — moves are explicit; you can't accidentally clone-by-copy. +- `#[derive(Clone, Zeroize, ZeroizeOnDrop)]` — bytes are wiped from RAM + when the value goes out of scope (volatile writes, defeating the + compiler's dead-store elimination). +- `Debug` — redacted as `Privkey()`. Bytes never leak through + formatting. + +The `privkey_debug_does_not_leak_bytes` integration test enforces the +redaction contract. + +## Error model + +Single error type with two variants: + +```rust +pub enum Error { + /// FFI returned a non-zero status code (the C contract is "0 on success, + /// -1 on failure"; we surface the raw value rather than coercing it). + NonZeroRc(i32), + + /// A post-condition the FFI promised wasn't met (e.g. the Send proof + /// utility wrote a different number of bytes than the spec advertises). + Invariant(&'static str), +} +``` + +The split is intentional: `NonZeroRc` means *"the C side told us it failed"*; +`Invariant` means *"the C side said it succeeded but the result violates an +assumption we baked in."* They imply different debugging paths. + +## Quick example + +```rust +use mpt_crypto::{keypair, encrypt, commit, context, prove, AccountId, IssuanceId}; + +let (sender_sk, sender_pk) = keypair::generate()?; +let r = encrypt::random_blinding_factor()?; + +// Encrypt the same amount under two different keys with the same r — +// matches the XLS-0096 §5.4 shared-randomness pattern. +let amount = 1_000; +let ct_under_sender = encrypt::encrypt(amount, &sender_pk, &r)?; + +// Pedersen commit + per-tx context hash + Convert-style Schnorr PoK. +let commitment = commit::pedersen(amount, &r)?; +let acct = AccountId::new([0u8; 20]); // real values come from xrpl-rust +let iss = IssuanceId::new([0u8; 24]); +let ctx = context::convert(&acct, &iss, /* sequence */ 1)?; +let proof = prove::convert(&sender_sk, &sender_pk, &ctx)?; +``` + +End-to-end working examples for all four transaction types live in +`tests/integration.rs`. + +## Running tests + +This crate has 14 integration tests (no inline unit tests; everything goes +through the public API). + +```bash +# All tests +cargo test -p mpt-crypto + +# Just the convert_back tests (positive + overdraft rejection) +cargo test -p mpt-crypto convert_back + +# A single test by full name +cargo test -p mpt-crypto convert_proof_rejects_wrong_pubkey + +# Show stdout/println output (otherwise hidden by libtest) +cargo test -p mpt-crypto -- --nocapture + +# Verbose: see linker invocations from build.rs +cargo test -p mpt-crypto -v +``` + +### What the tests verify + +| Group | What it covers | +|---|---| +| `keypair_*`, `encrypt_*` | ElGamal encrypt/decrypt round-trip | +| `pedersen_*` | Commitment determinism and binding | +| `*_debug_*` | Type-level redaction of secrets vs. abbreviation of public values | +| `context_hashes_*`, `send_context_hash_*` | Domain separation across transaction types and binding to destination | +| `convert_proof_verifies` | Schnorr PoK round-trip (prove → C verifier accepts) | +| `convert_proof_rejects_wrong_pubkey` | Convert proof binds to pk; can't be retargeted | +| `convert_back_proof_verifies` | 816 B composite proof round-trip | +| **`convert_back_proof_rejects_withdrawal_exceeding_balance`** | **No overdraft proof can be generated AND accepted** | +| `clawback_proof_verifies` | 64 B compact sigma round-trip | +| `send_proof_verifies_three_participants` | 946 B composite proof round-trip (no auditor) | +| `send_range_proof_verifies_extracted_subproof` | Lower-level range verifier on the Send proof's 754 B Bulletproof | +| `revealed_amount_verifies_matching_ciphertexts` | Cross-participant amount consistency (and rejection of a wrong amount) | +| `convert_back_remainder_matches_direct_commitment` | `pc_b − m·G` equals a direct commitment to `balance − m` | +| `send_range_proof_rejects_wrong_length`, `aggregated_bulletproof_rejects_empty_commitments` | Input-validation guards on the lower-level verifiers | + +Each proof round-trip pairs a [`prove`] generator with its [`verify`] +counterpart — both call into the same C primitives rippled uses, so a passing +round-trip means the FFI plumbing and the cryptographic algorithms are +coherent. The tests exercise only the safe surface (no `unsafe`, no direct +`mpt_crypto_sys` calls). + +### What's required to run tests + +- The `mpt-crypto-sys` crate must be buildable (see its + [README](../mpt-crypto-sys/README.md) for first-time setup; on a fresh + clone, `build.rs` will download `libmpt-crypto.{so,dylib,dll}` from the + pinned upstream release the first time you build). +- A network connection on first build, unless you've populated + `internal/mpt-crypto-sys/vendor/lib//` locally or set + `MPT_CRYPTO_LIB_DIR`. +- No `libclang` / `bindgen-cli` needed (the sys crate ships pre-generated + bindings). + +If something fails at runtime with "Library not loaded" / "error while +loading shared libraries", see the **Verifying the build linked correctly** +section of `internal/mpt-crypto-sys/README.md`. + +## Why `publish = false` + +This crate is a private workspace member of `xrpl-rust`. It depends on +`mpt-crypto-sys` via path, which is also unpublished. The intent is for +xrpl-rust transaction models to consume this crate internally, not for it +to be a standalone library on crates.io. + +If you need to depend on confidential-MPT crypto from outside xrpl-rust, +the recommended path is a git dependency on the parent xrpl-rust workspace. + +## See also + +- [`mpt-crypto-sys` README](../mpt-crypto-sys/README.md) — the FFI layer + this crate wraps; covers native-library distribution, build.rs internals, + and the bindgen regeneration procedure. +- [XLS-0096 spec](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0096-confidential-mpt) — + the cryptographic protocol this crate implements wrappers for. +- [mpt-crypto upstream](https://github.com/XRPLF/mpt-crypto) — the C + library; its `paper/cmpt-compact-sigma.pdf` is the formal spec for the + compact AND-composed sigma proof used by Send/ConvertBack/Clawback. diff --git a/internal/mpt-crypto/src/commit.rs b/internal/mpt-crypto/src/commit.rs new file mode 100644 index 00000000..2d33b313 --- /dev/null +++ b/internal/mpt-crypto/src/commit.rs @@ -0,0 +1,44 @@ +//! Pedersen commitments on secp256k1. + +use crate::{ + Error, Result, + types::{BlindingFactor, Commitment}, +}; +use mpt_crypto_sys as sys; + +/// Computes a Pedersen commitment `PC = amount·G + blinding·H`. +/// +/// `H` is a NUMS generator fixed by the upstream library; same `(amount, +/// blinding)` pair always produces the same commitment. +pub fn pedersen(amount: u64, blinding: &BlindingFactor) -> Result { + let mut out = [0u8; 33]; + // SAFETY: 32-byte input + 33-byte output match the FFI contract. + let rc = unsafe { + sys::mpt_get_pedersen_commitment(amount, blinding.as_bytes().as_ptr(), out.as_mut_ptr()) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(Commitment::new(out)) +} + +/// Subtracts a transparent `amount` from a hidden commitment, yielding the +/// remainder commitment `pc_rem = commitment_in - amount·G`. +/// +/// Lets a caller independently reconstruct the remainder a `ConvertBack` +/// proves non-negative (see [`crate::verify::convert_back`]). +pub fn convert_back_remainder(commitment_in: &Commitment, amount: u64) -> Result { + let mut out = [0u8; 33]; + // SAFETY: 33-byte input + 33-byte output match the FFI contract. + let rc = unsafe { + sys::mpt_compute_convert_back_remainder( + commitment_in.as_bytes().as_ptr(), + amount, + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(Commitment::new(out)) +} diff --git a/internal/mpt-crypto/src/context.rs b/internal/mpt-crypto/src/context.rs new file mode 100644 index 00000000..3ff34a28 --- /dev/null +++ b/internal/mpt-crypto/src/context.rs @@ -0,0 +1,136 @@ +//! Per-transaction-type context hashes. +//! +//! Each XLS-0096 confidential transaction binds its ZK proof to a 32-byte +//! transcript that includes the transaction type and the relevant ledger +//! identifiers. This prevents replay across contexts and produces clear +//! "wrong context = invalid proof" failures. +//! +//! The four functions here mirror the four context-hash variants the spec +//! defines — they differ in which ledger identifiers are folded into the +//! transcript: +//! +//! | Function | Inputs | +//! |---|---| +//! | [`convert`] | `account, issuance, sequence` | +//! | [`convert_back`] | `account, issuance, sequence, version` | +//! | [`send`] | `sender, issuance, sequence, destination, version` | +//! | [`clawback`] | `issuer, issuance, sequence, holder` | + +use crate::{ + Error, Result, + types::{AccountId, ContextHash, IssuanceId}, +}; +use mpt_crypto_sys as sys; + +#[inline] +fn into_sys_account(id: &AccountId) -> sys::account_id { + sys::account_id { + bytes: *id.as_bytes(), + } +} + +#[inline] +fn into_sys_issuance(id: &IssuanceId) -> sys::mpt_issuance_id { + sys::mpt_issuance_id { + bytes: *id.as_bytes(), + } +} + +/// Context hash for `ConfidentialMPTConvert`. +/// +/// Transcript includes the converting account, the MPTokenIssuance ID, and +/// the transaction sequence number. +pub fn convert(account: &AccountId, issuance: &IssuanceId, sequence: u32) -> Result { + let mut out = [0u8; 32]; + // SAFETY: account_id / mpt_issuance_id structs are passed by value and + // contain only fixed-size byte arrays; out buffer is 32 bytes. + let rc = unsafe { + sys::mpt_get_convert_context_hash( + into_sys_account(account), + into_sys_issuance(issuance), + sequence, + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(ContextHash::new(out)) +} + +/// Context hash for `ConfidentialMPTConvertBack`. Adds `version` to bind the +/// proof to a specific spending-balance state. +pub fn convert_back( + account: &AccountId, + issuance: &IssuanceId, + sequence: u32, + version: u32, +) -> Result { + let mut out = [0u8; 32]; + // SAFETY: see `convert`. + let rc = unsafe { + sys::mpt_get_convert_back_context_hash( + into_sys_account(account), + into_sys_issuance(issuance), + sequence, + version, + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(ContextHash::new(out)) +} + +/// Context hash for `ConfidentialMPTSend`. Binds the proof to a specific +/// `destination` so a sender can't re-target a generated proof. +pub fn send( + sender: &AccountId, + issuance: &IssuanceId, + sequence: u32, + destination: &AccountId, + version: u32, +) -> Result { + let mut out = [0u8; 32]; + // SAFETY: see `convert`. + let rc = unsafe { + sys::mpt_get_send_context_hash( + into_sys_account(sender), + into_sys_issuance(issuance), + sequence, + into_sys_account(destination), + version, + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(ContextHash::new(out)) +} + +/// Context hash for `ConfidentialMPTClawback`. Issuer-signed; binds to the +/// holder being clawed back. +pub fn clawback( + issuer: &AccountId, + issuance: &IssuanceId, + sequence: u32, + holder: &AccountId, +) -> Result { + let mut out = [0u8; 32]; + // SAFETY: see `convert`. + let rc = unsafe { + sys::mpt_get_clawback_context_hash( + into_sys_account(issuer), + into_sys_issuance(issuance), + sequence, + into_sys_account(holder), + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(ContextHash::new(out)) +} diff --git a/internal/mpt-crypto/src/encrypt.rs b/internal/mpt-crypto/src/encrypt.rs new file mode 100644 index 00000000..ed7a182a --- /dev/null +++ b/internal/mpt-crypto/src/encrypt.rs @@ -0,0 +1,66 @@ +//! EC-ElGamal encryption / decryption and blinding-factor generation. + +use crate::{ + Error, Result, + types::{BlindingFactor, Ciphertext, Privkey, Pubkey}, +}; +use mpt_crypto_sys as sys; + +/// Generates a fresh 32-byte blinding factor (the ElGamal randomness `r`). +/// +/// Used both as the ElGamal randomness across multi-recipient ciphertexts +/// and (in Send proofs) as the Pedersen blinding factor for `AmountCommitment` +/// — see XLS-0096 §5.4 "reused randomness" optimization. +pub fn random_blinding_factor() -> Result { + let mut r = [0u8; 32]; + // SAFETY: `r` is exclusively borrowed; size matches the 32-byte contract. + let rc = unsafe { sys::mpt_generate_blinding_factor(r.as_mut_ptr()) }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(BlindingFactor::new(r)) +} + +/// Encrypts a 64-bit `amount` under `pubkey` with the supplied `blinding`. +/// +/// Result is the 66-byte ciphertext `(R = r·G, S = m·G + r·pk)`. +/// Reusing the same blinding across multiple ciphertexts under different keys +/// produces "shared-r" ciphertexts that the compact sigma proof relies on. +pub fn encrypt(amount: u64, pubkey: &Pubkey, blinding: &BlindingFactor) -> Result { + let mut out = [0u8; 66]; + // SAFETY: pointers reference fixed-size arrays whose lengths match the + // FFI contract (33, 32, 66 bytes). + let rc = unsafe { + sys::mpt_encrypt_amount( + amount, + pubkey.as_bytes().as_ptr(), + blinding.as_bytes().as_ptr(), + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(Ciphertext::new(out)) +} + +/// Decrypts a ciphertext using the holder's secret key, recovering the +/// original `u64` amount. +/// +/// The C implementation uses a discrete-log lookup table for u64 — fast for +/// small / typical balances, slow if the recovered scalar is unusually large. +pub fn decrypt(ciphertext: &Ciphertext, privkey: &Privkey) -> Result { + let mut amount: u64 = 0; + // SAFETY: pointers are valid for the call; `&mut amount` is exclusive. + let rc = unsafe { + sys::mpt_decrypt_amount( + ciphertext.as_bytes().as_ptr(), + privkey.as_bytes().as_ptr(), + &mut amount, + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(amount) +} diff --git a/internal/mpt-crypto/src/error.rs b/internal/mpt-crypto/src/error.rs new file mode 100644 index 00000000..53fdd92a --- /dev/null +++ b/internal/mpt-crypto/src/error.rs @@ -0,0 +1,20 @@ +//! Single error type covering every failure mode in the safe wrapper layer. + +use thiserror::Error; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + /// An FFI call returned a non-zero status code. Upstream contract is + /// "0 on success, -1 on failure" — we surface the raw value for context + /// rather than coercing to a single "FfiFailure" variant. + #[error("mpt-crypto FFI returned non-zero status: {0}")] + NonZeroRc(i32), + + /// A post-condition the FFI promised wasn't met (e.g. the Send proof + /// utility wrote a different number of bytes than the spec advertises). + #[error("invariant violated: {0}")] + Invariant(&'static str), +} + +pub type Result = core::result::Result; diff --git a/internal/mpt-crypto/src/keypair.rs b/internal/mpt-crypto/src/keypair.rs new file mode 100644 index 00000000..5ca31288 --- /dev/null +++ b/internal/mpt-crypto/src/keypair.rs @@ -0,0 +1,25 @@ +//! ElGamal/secp256k1 keypair generation. + +use crate::{ + Error, Result, + types::{Privkey, Pubkey}, +}; +use mpt_crypto_sys as sys; + +/// Generates a fresh ElGamal keypair. +/// +/// Internally calls into libmpt-crypto, which uses OpenSSL's RNG (statically +/// linked into the dylib). The private key is wiped from memory when the +/// returned `Privkey` is dropped. +pub fn generate() -> Result<(Privkey, Pubkey)> { + let mut sk = [0u8; 32]; + let mut pk = [0u8; 33]; + + // SAFETY: `sk` and `pk` are mutable for the duration of the call; + // their sizes match the FFI contract (32 / 33 bytes). + let rc = unsafe { sys::mpt_generate_keypair(sk.as_mut_ptr(), pk.as_mut_ptr()) }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok((Privkey::new(sk), Pubkey::new(pk))) +} diff --git a/internal/mpt-crypto/src/lib.rs b/internal/mpt-crypto/src/lib.rs new file mode 100644 index 00000000..1856d007 --- /dev/null +++ b/internal/mpt-crypto/src/lib.rs @@ -0,0 +1,45 @@ +//! Safe Rust wrappers around [`mpt_crypto_sys`] for XLS-0096 Confidential MPTs. +//! +//! The sys crate exposes ~88 raw `unsafe extern "C"` functions; this crate +//! curates the subset a client library actually needs, with idiomatic Rust +//! types, `Result`-based errors, and `Zeroize`-on-drop secrets. +//! +//! # Module map +//! +//! | Module | Purpose | +//! |---|---| +//! | [`keypair`] | Generate ElGamal holder keys | +//! | [`encrypt`] | Encrypt / decrypt amounts; produce blinding factors | +//! | [`commit`] | Pedersen commitments | +//! | [`context`] | Per-transaction-type context hashes (replay binding) | +//! | [`prove`] | Generate the four per-transaction-type proofs | +//! | [`verify`] | Verify the four proofs + revealed-amount / range checks | +//! | [`types`] | Strongly-typed byte-array wrappers (Privkey, Pubkey, etc.) | +//! | [`error`] | [`Error`] and [`Result`] | +//! +//! # Example +//! +//! ```ignore +//! // `ignore`: this code is for documentation only — see Cargo.toml's +//! // `doctest = false`. Real end-to-end coverage lives in +//! // `tests/integration.rs`. +//! use mpt_crypto::{keypair, encrypt}; +//! +//! let (sk, pk) = keypair::generate().unwrap(); +//! let r = encrypt::random_blinding_factor().unwrap(); +//! let ct = encrypt::encrypt(1_000, &pk, &r).unwrap(); +//! let m = encrypt::decrypt(&ct, &sk).unwrap(); +//! assert_eq!(m, 1_000); +//! ``` + +pub mod commit; +pub mod context; +pub mod encrypt; +pub mod error; +pub mod keypair; +pub mod prove; +pub mod types; +pub mod verify; + +pub use error::{Error, Result}; +pub use types::*; diff --git a/internal/mpt-crypto/src/prove.rs b/internal/mpt-crypto/src/prove.rs new file mode 100644 index 00000000..76e8dbb2 --- /dev/null +++ b/internal/mpt-crypto/src/prove.rs @@ -0,0 +1,237 @@ +//! Per-transaction-type proof generation. +//! +//! These are the four user-facing prove functions matching the four +//! proof-bearing transaction types in XLS-0096: +//! +//! | Function | Output | XLS-0096 reference | +//! |---|---|---| +//! | [`convert`] | [`ConvertProof`] (64 B Schnorr PoK) | §7.2 | +//! | [`send`] | [`SendProof`] (946 B composite) | §8.2 / §5.4 | +//! | [`convert_back`] | [`ConvertBackProof`] (816 B composite) | §10.3 | +//! | [`clawback`] | [`ClawbackProof`] (64 B compact sigma) | §11.2 | +//! +//! The `*Params` structs collect the long argument lists into struct-init +//! syntax, which makes call sites self-documenting and avoids parameter- +//! ordering bugs. + +use crate::{ + Error, Result, + types::{ + BlindingFactor, Ciphertext, ClawbackProof, Commitment, ContextHash, ConvertBackProof, + ConvertProof, Privkey, Pubkey, SendProof, + }, +}; +use mpt_crypto_sys as sys; + +/// One participant in a confidential transfer: their public key plus the +/// ciphertext of the transfer amount under that key. +#[derive(Debug, Clone, Copy)] +pub struct Participant<'a> { + pub pubkey: &'a Pubkey, + pub ciphertext: &'a Ciphertext, +} + +// ───────────────────────────────────────────────────────────────────────── +// Convert: 64-byte Schnorr Proof of Knowledge +// ───────────────────────────────────────────────────────────────────────── + +/// Generates the 64-byte Schnorr Proof of Knowledge required at first +/// `ConfidentialMPTConvert` (when registering the holder's `pk`). +/// +/// Returns `Err(NonZeroRc(_))` if the underlying primitive fails — typically +/// indicates a malformed pubkey. +pub fn convert(privkey: &Privkey, pubkey: &Pubkey, ctx: &ContextHash) -> Result { + let mut out = [0u8; 64]; + // SAFETY: pointer arguments target fixed-size buffers matching the FFI + // contract (33-byte pubkey, 32-byte privkey, 32-byte ctx hash, + // 64-byte output). + let rc = unsafe { + sys::mpt_get_convert_proof( + pubkey.as_bytes().as_ptr(), + privkey.as_bytes().as_ptr(), + ctx.as_bytes().as_ptr(), + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(ConvertProof::new(out)) +} + +// ───────────────────────────────────────────────────────────────────────── +// Send: 946-byte composite (192 B compact sigma + 754 B aggregated BP) +// ───────────────────────────────────────────────────────────────────────── + +/// Inputs to [`send`]. Lots of fields because the Send ZK statement has +/// many witnesses. +/// +/// **Critical invariant** (XLS-0096 §5.4): `tx_blinding_factor` is the same +/// scalar used both as the ElGamal randomness `r` for *all* participant +/// ciphertexts AND as the Pedersen blinding for `amount_commitment`. The +/// compact sigma collapses the amount-linkage proof into the main sigma by +/// reusing this scalar. Pass an independent value here and verification will +/// fail. +/// +/// `balance_ciphertext` must be the holder's `CB_S` **as it currently lives +/// on the ledger** (not a fresh encryption of `current_balance`). The proof +/// links the new `balance_commitment` to this on-chain ciphertext via the +/// holder's secret key. +pub struct SendProofParams<'a> { + pub sender_privkey: &'a Privkey, + pub sender_pubkey: &'a Pubkey, + pub amount: u64, + pub current_balance: u64, + pub tx_blinding_factor: &'a BlindingFactor, + pub context_hash: &'a ContextHash, + pub amount_commitment: &'a Commitment, + pub balance_commitment: &'a Commitment, + pub balance_blinding: &'a BlindingFactor, + pub balance_ciphertext: &'a Ciphertext, + pub sender: Participant<'a>, + pub destination: Participant<'a>, + pub issuer: Participant<'a>, + pub auditor: Option>, +} + +/// Generates the 946-byte `ConfidentialMPTSend` proof bundle. +pub fn send(p: SendProofParams<'_>) -> Result { + // The C side expects a contiguous array of `mpt_confidential_participant`. + // We build it on the stack — at most 4 entries. + let mut participants = [sys::mpt_confidential_participant { + pubkey: [0; 33], + ciphertext: [0; 66], + }; 4]; + + fn fill(slot: &mut sys::mpt_confidential_participant, p: Participant<'_>) { + slot.pubkey = *p.pubkey.as_bytes(); + slot.ciphertext = *p.ciphertext.as_bytes(); + } + + fill(&mut participants[0], p.sender); + fill(&mut participants[1], p.destination); + fill(&mut participants[2], p.issuer); + + let n = if let Some(aud) = p.auditor { + fill(&mut participants[3], aud); + 4 + } else { + 3 + }; + + let balance_params = sys::mpt_pedersen_proof_params { + pedersen_commitment: *p.balance_commitment.as_bytes(), + amount: p.current_balance, + ciphertext: *p.balance_ciphertext.as_bytes(), + blinding_factor: *p.balance_blinding.as_bytes(), + }; + + let mut out = [0u8; 946]; + let mut out_len: usize = 946; + + // SAFETY: All buffers are sized to the FFI contract; participants array + // length matches the `n` we pass. `&balance_params` is borrowed + // exclusively for the call. + let rc = unsafe { + sys::mpt_get_confidential_send_proof( + p.sender_privkey.as_bytes().as_ptr(), + p.sender_pubkey.as_bytes().as_ptr(), + p.amount, + participants.as_ptr(), + n, + p.tx_blinding_factor.as_bytes().as_ptr(), + p.context_hash.as_bytes().as_ptr(), + p.amount_commitment.as_bytes().as_ptr(), + &balance_params, + out.as_mut_ptr(), + &mut out_len, + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + if out_len != 946 { + return Err(Error::Invariant("send proof had unexpected length")); + } + Ok(SendProof::new(out)) +} + +// ───────────────────────────────────────────────────────────────────────── +// ConvertBack: 816-byte composite (128 B compact sigma + 688 B BP) +// ───────────────────────────────────────────────────────────────────────── + +/// Inputs to [`convert_back`]. +/// +/// `balance_ciphertext` must be the holder's on-ledger `CB_S` ciphertext — +/// same constraint as in [`SendProofParams`]. `amount` is the publicly +/// revealed plaintext withdrawal amount. +pub struct ConvertBackProofParams<'a> { + pub holder_privkey: &'a Privkey, + pub holder_pubkey: &'a Pubkey, + pub amount: u64, + pub current_balance: u64, + pub context_hash: &'a ContextHash, + pub balance_commitment: &'a Commitment, + pub balance_blinding: &'a BlindingFactor, + pub balance_ciphertext: &'a Ciphertext, +} + +/// Generates the 816-byte `ConfidentialMPTConvertBack` proof bundle. +pub fn convert_back(p: ConvertBackProofParams<'_>) -> Result { + let params = sys::mpt_pedersen_proof_params { + pedersen_commitment: *p.balance_commitment.as_bytes(), + amount: p.current_balance, + ciphertext: *p.balance_ciphertext.as_bytes(), + blinding_factor: *p.balance_blinding.as_bytes(), + }; + let mut out = [0u8; 816]; + // SAFETY: see `send`. + let rc = unsafe { + sys::mpt_get_convert_back_proof( + p.holder_privkey.as_bytes().as_ptr(), + p.holder_pubkey.as_bytes().as_ptr(), + p.context_hash.as_bytes().as_ptr(), + p.amount, + ¶ms, + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(ConvertBackProof::new(out)) +} + +// ───────────────────────────────────────────────────────────────────────── +// Clawback: 64-byte compact sigma proof +// ───────────────────────────────────────────────────────────────────────── + +/// Generates the 64-byte `ConfidentialMPTClawback` proof. +/// +/// Issuer-only. Proves that the holder's `IssuerEncryptedBalance` ciphertext +/// (already on the ledger) decrypts to `amount` under the issuer's secret +/// key. +pub fn clawback( + issuer_privkey: &Privkey, + issuer_pubkey: &Pubkey, + context_hash: &ContextHash, + amount: u64, + issuer_encrypted_balance: &Ciphertext, +) -> Result { + let mut out = [0u8; 64]; + // SAFETY: 32 / 33 / 32 / 66 / 64 buffer sizes match the FFI contract. + let rc = unsafe { + sys::mpt_get_clawback_proof( + issuer_privkey.as_bytes().as_ptr(), + issuer_pubkey.as_bytes().as_ptr(), + context_hash.as_bytes().as_ptr(), + amount, + issuer_encrypted_balance.as_bytes().as_ptr(), + out.as_mut_ptr(), + ) + }; + if rc != 0 { + return Err(Error::NonZeroRc(rc)); + } + Ok(ClawbackProof::new(out)) +} diff --git a/internal/mpt-crypto/src/types.rs b/internal/mpt-crypto/src/types.rs new file mode 100644 index 00000000..4a78355c --- /dev/null +++ b/internal/mpt-crypto/src/types.rs @@ -0,0 +1,158 @@ +//! Strongly-typed byte-array wrappers used across the safe API. +//! +//! Public-information types ([`Pubkey`], [`Ciphertext`], [`Commitment`], +//! [`ContextHash`], [`AccountId`], [`IssuanceId`], the per-tx `*Proof` +//! types) are `Copy` newtypes you can construct, compare, and serialize +//! freely. +//! +//! Secret types ([`Privkey`], [`BlindingFactor`]) implement [`Zeroize`] and +//! [`ZeroizeOnDrop`], have **redacted** [`Debug`] output, and are not `Copy` +//! — their bytes are wiped from memory when they go out of scope. + +use core::fmt; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +// ───────────────────────────────────────────────────────────────────────── +// Macros to reduce boilerplate +// ───────────────────────────────────────────────────────────────────────── + +/// Public newtype: `Copy + Eq + Debug-as-hex-prefix`. +macro_rules! public_bytes { + ($name:ident, $size:expr, $doc:literal) => { + #[doc = $doc] + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct $name(pub [u8; $size]); + + impl $name { + #[inline] + pub const fn new(bytes: [u8; $size]) -> Self { + Self(bytes) + } + #[inline] + pub const fn as_bytes(&self) -> &[u8; $size] { + &self.0 + } + #[inline] + pub const fn into_bytes(self) -> [u8; $size] { + self.0 + } + } + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}({})", stringify!($name), hex_short(&self.0)) + } + } + }; +} + +/// Secret newtype: `Zeroize + ZeroizeOnDrop`, no `Copy`, redacted `Debug`. +macro_rules! secret_bytes { + ($name:ident, $size:expr, $doc:literal) => { + #[doc = $doc] + #[derive(Clone, Zeroize, ZeroizeOnDrop)] + pub struct $name(pub(crate) [u8; $size]); + + impl $name { + #[inline] + pub fn new(bytes: [u8; $size]) -> Self { + Self(bytes) + } + #[inline] + pub fn as_bytes(&self) -> &[u8; $size] { + &self.0 + } + } + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(concat!(stringify!($name), "()")) + } + } + }; +} + +// ───────────────────────────────────────────────────────────────────────── +// Identity / addressing +// ───────────────────────────────────────────────────────────────────────── + +public_bytes!(AccountId, 20, "20-byte XRPL AccountID."); +public_bytes!(IssuanceId, 24, "24-byte XRPL `MPTokenIssuanceID`."); + +// ───────────────────────────────────────────────────────────────────────── +// Key material +// ───────────────────────────────────────────────────────────────────────── + +secret_bytes!( + Privkey, + 32, + "32-byte ElGamal/secp256k1 secret key. Zeroized on drop." +); +public_bytes!(Pubkey, 33, "33-byte compressed secp256k1 public key."); +secret_bytes!( + BlindingFactor, + 32, + "32-byte ElGamal randomness / Pedersen blinding factor. Zeroized on drop." +); + +// ───────────────────────────────────────────────────────────────────────── +// Cryptographic objects +// ───────────────────────────────────────────────────────────────────────── + +public_bytes!( + Ciphertext, + 66, + "66-byte EC-ElGamal ciphertext (two compressed points: R || S)." +); +public_bytes!(Commitment, 33, "33-byte Pedersen commitment."); +public_bytes!( + ContextHash, + 32, + "32-byte SHA-256 transcript hash binding a proof to its transaction." +); + +// ───────────────────────────────────────────────────────────────────────── +// Proof blobs (sized exactly per XLS-0096 §5.4) +// ───────────────────────────────────────────────────────────────────────── + +public_bytes!( + ConvertProof, + 64, + "64-byte Schnorr Proof of Knowledge (ConfidentialMPTConvert)." +); +public_bytes!( + SendProof, + 946, + "946-byte ConfidentialMPTSend proof: 192 B compact sigma + 754 B aggregated Bulletproof." +); +public_bytes!( + ConvertBackProof, + 816, + "816-byte ConfidentialMPTConvertBack proof: 128 B compact sigma + 688 B Bulletproof." +); +public_bytes!( + ClawbackProof, + 64, + "64-byte ConfidentialMPTClawback compact sigma proof." +); + +// ───────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────── + +/// Hex-encode the first 4 and last 4 bytes for a Debug summary. +fn hex_short(bytes: &[u8]) -> String { + if bytes.len() <= 8 { + bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + } else { + let head: String = bytes[..4].iter().map(|b| format!("{:02x}", b)).collect(); + let tail: String = bytes[bytes.len() - 4..] + .iter() + .map(|b| format!("{:02x}", b)) + .collect(); + format!("0x{head}…{tail}") + } +} diff --git a/internal/mpt-crypto/src/verify.rs b/internal/mpt-crypto/src/verify.rs new file mode 100644 index 00000000..3f329efe --- /dev/null +++ b/internal/mpt-crypto/src/verify.rs @@ -0,0 +1,310 @@ +//! Per-transaction-type proof verification. +//! +//! These are the safe counterparts to the [`crate::prove`] generators: each +//! function checks a proof against its public inputs and returns `Ok(())` +//! **iff** the proof is valid. +//! +//! | Function | Verifies | Generated by | +//! |---|---|---| +//! | [`convert`] | [`ConvertProof`] | [`prove::convert`](crate::prove::convert) | +//! | [`send`] | [`SendProof`] | [`prove::send`](crate::prove::send) | +//! | [`convert_back`] | [`ConvertBackProof`] | [`prove::convert_back`](crate::prove::convert_back) | +//! | [`clawback`] | [`ClawbackProof`] | [`prove::clawback`](crate::prove::clawback) | +//! | [`revealed_amount`] | matching ciphertexts | — | +//! +//! Two lower-level range-proof checks — [`send_range_proof`] and +//! [`aggregated_bulletproof`] — are also exposed; the transaction-level +//! [`send`]/[`convert_back`] verifiers already cover these internally, so most +//! callers will not need them directly. +//! +//! # Return semantics +//! +//! Every verifier returns `Result<()>`: +//! +//! * `Ok(())` — the proof verified. +//! * `Err(_)` — verification failed. The underlying C contract returns the +//! same non-zero status for *both* an algebraically-invalid proof **and** +//! malformed input, so a verifier cannot distinguish the two; either way the +//! correct response is to reject. Callers wanting a boolean can use +//! [`Result::is_ok`]. +//! +//! This "fail-closed" shape (anything other than a clean success is an `Err`) +//! is the same convention used by signature-verification APIs elsewhere in the +//! Rust ecosystem. + +use crate::{ + Error, Result, + prove::Participant, + types::{ + BlindingFactor, Ciphertext, ClawbackProof, Commitment, ContextHash, ConvertBackProof, + ConvertProof, Pubkey, SendProof, + }, +}; +use mpt_crypto_sys as sys; + +/// Length, in bytes, of the double-Bulletproof blob accepted by +/// [`send_range_proof`] (XLS-0096 §5.4). +const SEND_RANGE_PROOF_LEN: usize = 754; + +/// Translate a safe [`Participant`] into the C ABI struct. +fn to_ffi(p: Participant<'_>) -> sys::mpt_confidential_participant { + sys::mpt_confidential_participant { + pubkey: *p.pubkey.as_bytes(), + ciphertext: *p.ciphertext.as_bytes(), + } +} + +/// Map the C `0 = success / non-zero = failure` contract onto `Result`. +#[inline] +fn check(rc: i32) -> Result<()> { + if rc == 0 { + Ok(()) + } else { + Err(Error::NonZeroRc(rc)) + } +} + +// ───────────────────────────────────────────────────────────────────────── +// Convert: 64-byte Schnorr Proof of Knowledge +// ───────────────────────────────────────────────────────────────────────── + +/// Verifies a `ConfidentialMPTConvert` proof: that the holder knows the secret +/// key for `pubkey`, bound to `ctx`. +/// +/// Re-deriving the Fiat-Shamir challenge from `pubkey` is what makes this +/// reject a proof presented against any *other* holder's public key. +pub fn convert(proof: &ConvertProof, pubkey: &Pubkey, ctx: &ContextHash) -> Result<()> { + // SAFETY: 64-byte proof, 33-byte pubkey, 32-byte ctx hash match the FFI + // contract; all are read-only. + let rc = unsafe { + sys::mpt_verify_convert_proof( + proof.as_bytes().as_ptr(), + pubkey.as_bytes().as_ptr(), + ctx.as_bytes().as_ptr(), + ) + }; + check(rc) +} + +// ───────────────────────────────────────────────────────────────────────── +// ConvertBack: 816-byte composite +// ───────────────────────────────────────────────────────────────────────── + +/// Inputs to [`convert_back`]. Mirrors the public half of +/// [`prove::ConvertBackProofParams`](crate::prove::ConvertBackProofParams) — +/// no secrets are needed to verify. +/// +/// `balance_ciphertext` is the holder's on-ledger `CB_S`; `balance_commitment` +/// the Pedersen commitment carried in the transaction; `amount` the publicly +/// revealed withdrawal. +pub struct ConvertBackVerifyParams<'a> { + pub proof: &'a ConvertBackProof, + pub holder_pubkey: &'a Pubkey, + pub balance_ciphertext: &'a Ciphertext, + pub balance_commitment: &'a Commitment, + pub amount: u64, + pub context_hash: &'a ContextHash, +} + +/// Verifies a `ConfidentialMPTConvertBack` proof: ownership of the balance, +/// commitment/ciphertext linkage, and a non-negative remainder +/// (`balance - amount >= 0`). +pub fn convert_back(p: ConvertBackVerifyParams<'_>) -> Result<()> { + // SAFETY: 816-byte proof, 33-byte pubkey, 66-byte ciphertext, 33-byte + // commitment, 32-byte ctx hash match the FFI contract. + let rc = unsafe { + sys::mpt_verify_convert_back_proof( + p.proof.as_bytes().as_ptr(), + p.holder_pubkey.as_bytes().as_ptr(), + p.balance_ciphertext.as_bytes().as_ptr(), + p.balance_commitment.as_bytes().as_ptr(), + p.amount, + p.context_hash.as_bytes().as_ptr(), + ) + }; + check(rc) +} + +// ───────────────────────────────────────────────────────────────────────── +// Send: 946-byte composite +// ───────────────────────────────────────────────────────────────────────── + +/// Inputs to [`send`]. The participant list must be passed in the **same +/// order used when generating the proof** (sender, destination, issuer, then +/// optional auditor) — `participants[0]` is the sender. +pub struct SendVerifyParams<'a> { + pub proof: &'a SendProof, + pub sender: Participant<'a>, + pub destination: Participant<'a>, + pub issuer: Participant<'a>, + pub auditor: Option>, + /// The sender's on-ledger balance ciphertext (`CB_S`, `b1 || b2`). + pub sender_spending_ciphertext: &'a Ciphertext, + pub amount_commitment: &'a Commitment, + pub balance_commitment: &'a Commitment, + pub context_hash: &'a ContextHash, +} + +/// Verifies a `ConfidentialMPTSend` proof across all participants. +pub fn send(p: SendVerifyParams<'_>) -> Result<()> { + // Build the contiguous participant array the C side expects (≤ 4 entries), + // matching the ordering in `prove::send`. + let mut participants = [sys::mpt_confidential_participant { + pubkey: [0; 33], + ciphertext: [0; 66], + }; 4]; + + participants[0] = to_ffi(p.sender); + participants[1] = to_ffi(p.destination); + participants[2] = to_ffi(p.issuer); + + let n: u8 = if let Some(aud) = p.auditor { + participants[3] = to_ffi(aud); + 4 + } else { + 3 + }; + + // SAFETY: the participants array holds exactly `n` initialised entries and + // outlives the call; all other buffers match the FFI contract. + let rc = unsafe { + sys::mpt_verify_send_proof( + p.proof.as_bytes().as_ptr(), + participants.as_ptr(), + n, + p.sender_spending_ciphertext.as_bytes().as_ptr(), + p.amount_commitment.as_bytes().as_ptr(), + p.balance_commitment.as_bytes().as_ptr(), + p.context_hash.as_bytes().as_ptr(), + ) + }; + check(rc) +} + +// ───────────────────────────────────────────────────────────────────────── +// Clawback: 64-byte compact sigma proof +// ───────────────────────────────────────────────────────────────────────── + +/// Verifies a `ConfidentialMPTClawback` proof: that `ciphertext` decrypts to +/// exactly `amount` under the issuer's secret key. +pub fn clawback( + proof: &ClawbackProof, + amount: u64, + issuer_pubkey: &Pubkey, + ciphertext: &Ciphertext, + context_hash: &ContextHash, +) -> Result<()> { + // SAFETY: 64-byte proof, 33-byte pubkey, 66-byte ciphertext, 32-byte ctx + // hash match the FFI contract. + let rc = unsafe { + sys::mpt_verify_clawback_proof( + proof.as_bytes().as_ptr(), + amount, + issuer_pubkey.as_bytes().as_ptr(), + ciphertext.as_bytes().as_ptr(), + context_hash.as_bytes().as_ptr(), + ) + }; + check(rc) +} + +// ───────────────────────────────────────────────────────────────────────── +// Revealed-amount consistency +// ───────────────────────────────────────────────────────────────────────── + +/// Verifies that the holder's, issuer's and (optional) auditor's ciphertexts +/// all encrypt the same revealed `amount` under the shared ElGamal randomness +/// `blinding`. +pub fn revealed_amount( + amount: u64, + blinding: &BlindingFactor, + holder: Participant<'_>, + issuer: Participant<'_>, + auditor: Option>, +) -> Result<()> { + let holder_ffi = to_ffi(holder); + let issuer_ffi = to_ffi(issuer); + let auditor_ffi = auditor.map(to_ffi); + + // A null `auditor` pointer signals "no auditor" to the C side. + let auditor_ptr = auditor_ffi + .as_ref() + .map_or(core::ptr::null(), |a| a as *const _); + + // SAFETY: `blinding` is 32 bytes; the three participant pointers either + // reference live stack values for the duration of the call or are + // null (auditor), as the contract permits. + let rc = unsafe { + sys::mpt_verify_revealed_amount( + amount, + blinding.as_bytes().as_ptr(), + &holder_ffi, + &issuer_ffi, + auditor_ptr, + ) + }; + check(rc) +} + +// ───────────────────────────────────────────────────────────────────────── +// Lower-level range-proof checks +// ───────────────────────────────────────────────────────────────────────── + +/// Verifies the 754-byte double Bulletproof that bounds both the transfer +/// amount and the sender's remaining balance to `[0, 2^64)` (the range +/// component of a [`SendProof`]). +/// +/// Returns [`Error::Invariant`] if `proof` is not exactly +/// `SEND_RANGE_PROOF_LEN` bytes — the C verifier reads a fixed-size buffer. +pub fn send_range_proof( + proof: &[u8], + amount_commitment: &Commitment, + balance_commitment: &Commitment, + context_hash: &ContextHash, +) -> Result<()> { + if proof.len() != SEND_RANGE_PROOF_LEN { + return Err(Error::Invariant("send range proof must be 754 bytes")); + } + // SAFETY: `proof` is verified to be exactly 754 bytes; commitments are + // 33 bytes and the ctx hash 32 bytes per the FFI contract. + let rc = unsafe { + sys::mpt_verify_send_range_proof( + proof.as_ptr(), + amount_commitment.as_bytes().as_ptr(), + balance_commitment.as_bytes().as_ptr(), + context_hash.as_bytes().as_ptr(), + ) + }; + check(rc) +} + +/// Generic verifier for an aggregated Bulletproof over `commitments`. +/// +/// Returns [`Error::Invariant`] if `commitments` is empty. +pub fn aggregated_bulletproof( + proof: &[u8], + commitments: &[Commitment], + context_hash: &ContextHash, +) -> Result<()> { + if commitments.is_empty() { + return Err(Error::Invariant( + "aggregated bulletproof needs at least one commitment", + )); + } + // The C side takes an array of pointers to the 33-byte commitments. + let mut ptrs: Vec<*const u8> = commitments.iter().map(|c| c.as_bytes().as_ptr()).collect(); + + // SAFETY: `proof`/`proof_len` describe the same buffer; `ptrs` holds + // exactly `commitments.len()` valid pointers that outlive the + // call; the ctx hash is 32 bytes. + let rc = unsafe { + sys::mpt_verify_aggregated_bulletproof( + proof.as_ptr(), + proof.len(), + ptrs.as_mut_ptr(), + ptrs.len(), + context_hash.as_bytes().as_ptr(), + ) + }; + check(rc) +} diff --git a/internal/mpt-crypto/tests/integration.rs b/internal/mpt-crypto/tests/integration.rs new file mode 100644 index 00000000..89b20a48 --- /dev/null +++ b/internal/mpt-crypto/tests/integration.rs @@ -0,0 +1,1019 @@ +//! Integration tests for the safe wrapper. +//! +//! Each test exercises one slice of the API end-to-end. Where the safe +//! wrapper produces a proof, we check it via the corresponding +//! [`mpt_crypto::verify`] function — which calls the same C verifier rippled +//! uses, so it is the oracle for "did our prove call produce something the +//! ledger would accept?". These tests use only the safe surface: no `unsafe` +//! and no direct `mpt_crypto_sys` calls. + +use mpt_crypto::*; + +// ───────────────────────────────────────────────────────────────────────── +// Encryption round-trip +// ───────────────────────────────────────────────────────────────────────── + +#[test] +fn keypair_encrypt_decrypt_roundtrip() { + let (sk, pk) = keypair::generate().expect("keypair"); + let r = encrypt::random_blinding_factor().expect("blinding"); + let ct = encrypt::encrypt(1234, &pk, &r).expect("encrypt"); + let m = encrypt::decrypt(&ct, &sk).expect("decrypt"); + assert_eq!(m, 1234); +} + +#[test] +fn encrypt_with_same_blinding_is_deterministic() { + let (_sk, pk) = keypair::generate().unwrap(); + let r = encrypt::random_blinding_factor().unwrap(); + + let ct1 = encrypt::encrypt(42, &pk, &r).unwrap(); + let ct2 = encrypt::encrypt(42, &pk, &r).unwrap(); + assert_eq!( + ct1.as_bytes(), + ct2.as_bytes(), + "same (m, r, pk) should produce identical ciphertext" + ); +} + +#[test] +fn pubkey_debug_is_truncated_for_readability() { + // `Pubkey` is public information — its `Debug` impl truncates for + // readability (first 4 + last 4 bytes), not for security. Contrast with + // `privkey_debug_does_not_leak_bytes` below, which checks actual + // redaction of secret material. + let (_sk, pk) = keypair::generate().unwrap(); + let s = format!("{pk:?}"); + assert!(s.starts_with("Pubkey(")); + assert!(s.len() < 30, "Debug output unexpectedly long: {s}"); +} + +#[test] +fn privkey_debug_does_not_leak_bytes() { + let (sk, _pk) = keypair::generate().unwrap(); + let s = format!("{sk:?}"); + assert!( + s.contains("redacted"), + "Privkey Debug must not expose bytes" + ); + // The hex of any bit of the actual key should not appear in the Debug output. + let hex: String = sk.as_bytes().iter().map(|b| format!("{b:02x}")).collect(); + assert!( + !s.contains(&hex[0..8]), + "Privkey Debug output appears to contain key bytes" + ); +} + +// ───────────────────────────────────────────────────────────────────────── +// Pedersen commitment determinism +// ───────────────────────────────────────────────────────────────────────── + +#[test] +fn pedersen_commitment_is_deterministic() { + let r = encrypt::random_blinding_factor().unwrap(); + let c1 = commit::pedersen(7, &r).unwrap(); + let c2 = commit::pedersen(7, &r).unwrap(); + assert_eq!(c1, c2, "same (amount, blinding) should commit identically"); +} + +#[test] +fn pedersen_commitment_changes_with_amount() { + let r = encrypt::random_blinding_factor().unwrap(); + let c1 = commit::pedersen(7, &r).unwrap(); + let c2 = commit::pedersen(8, &r).unwrap(); + assert_ne!(c1, c2); +} + +/// `convert_back_remainder` computes `pc_rem = pc_b - m·G`. Since +/// `pc_b = balance·G + ρ·H`, the remainder must equal a fresh commitment to +/// `(balance - m)` under the same blinding `ρ`. This pins the helper to its +/// algebraic definition without needing a proof. +#[test] +fn convert_back_remainder_matches_direct_commitment() { + let balance = 1_000u64; + let withdraw = 250u64; + let rho = encrypt::random_blinding_factor().unwrap(); + + let pc_b = commit::pedersen(balance, &rho).unwrap(); + let remainder = commit::convert_back_remainder(&pc_b, withdraw).unwrap(); + let direct = commit::pedersen(balance - withdraw, &rho).unwrap(); + + assert_eq!( + remainder, direct, + "remainder commitment did not match a direct commitment to (balance - amount)" + ); +} + +// ───────────────────────────────────────────────────────────────────────── +// Context hash variants are distinct +// ───────────────────────────────────────────────────────────────────────── + +#[test] +fn context_hashes_differ_per_transaction_type() { + let acc = AccountId::new([1; 20]); + let iss = IssuanceId::new([2; 24]); + let dst = AccountId::new([3; 20]); + let seq = 100; + let ver = 5; + + let h_convert = context::convert(&acc, &iss, seq).unwrap(); + let h_convert_back = context::convert_back(&acc, &iss, seq, ver).unwrap(); + let h_send = context::send(&acc, &iss, seq, &dst, ver).unwrap(); + let h_clawback = context::clawback(&acc, &iss, seq, &dst).unwrap(); + + let all = [h_convert, h_convert_back, h_send, h_clawback]; + for i in 0..all.len() { + for j in (i + 1)..all.len() { + assert_ne!( + all[i], all[j], + "context hashes for different tx types collided ({i} vs {j})" + ); + } + } +} + +#[test] +fn send_context_hash_binds_to_destination() { + let snd = AccountId::new([1; 20]); + let iss = IssuanceId::new([2; 24]); + let d1 = AccountId::new([3; 20]); + let d2 = AccountId::new([4; 20]); + let h1 = context::send(&snd, &iss, 1, &d1, 0).unwrap(); + let h2 = context::send(&snd, &iss, 1, &d2, 0).unwrap(); + assert_ne!(h1, h2, "Send proof must be unforgeable across destinations"); +} + +// ───────────────────────────────────────────────────────────────────────── +// Convert proof — Schnorr PoK round-trip via the safe verifier +// ───────────────────────────────────────────────────────────────────────── + +#[test] +fn convert_proof_verifies() { + let (sk, pk) = keypair::generate().unwrap(); + let acc = AccountId::new([1; 20]); + let iss = IssuanceId::new([2; 24]); + let ctx = context::convert(&acc, &iss, 1).unwrap(); + + let proof = prove::convert(&sk, &pk, &ctx).unwrap(); + + verify::convert(&proof, &pk, &ctx).expect("convert proof failed verification"); +} + +#[test] +fn convert_proof_rejects_wrong_pubkey() { + // A valid proof bound to pk_A must not verify against an unrelated pubkey. + // + // The Fiat-Shamir challenge is c = H(pk, R, ctx, …). The verifier + // recomputes c using whichever `pk` it's given, so swapping the pubkey + // at verify time changes the challenge and breaks the algebraic check + // z·G == R + c·pk. This is the binding property that makes a Convert + // proof unforgeable against another holder's account. + // + // This is exactly the misuse the safe `verify` API is meant to guard + // against — Custody can branch on the `Result` instead of an `unsafe` + // status code. + + let (sk_a, pk_a) = keypair::generate().unwrap(); // the real keypair + let (_, wrong_pk) = keypair::generate().unwrap(); // an unrelated pubkey + + let acc = AccountId::new([1; 20]); + let iss = IssuanceId::new([2; 24]); + let ctx = context::convert(&acc, &iss, 1).unwrap(); + + // Generate a *correct* proof for (sk_a, pk_a). + let proof = prove::convert(&sk_a, &pk_a, &ctx).unwrap(); + + // Sanity check: against its own pubkey, the proof verifies. Without this, + // the rejection below could be hiding a bug where prove silently failed. + verify::convert(&proof, &pk_a, &ctx).expect("valid proof should verify against its own pubkey"); + + // The actual property: same proof, verified against a different pubkey, + // must be rejected. + assert!( + verify::convert(&proof, &wrong_pk, &ctx).is_err(), + "verifier accepted a valid proof against an unrelated pubkey" + ); +} + +// ───────────────────────────────────────────────────────────────────────── +// Clawback proof — round-trip via the safe verifier +// ───────────────────────────────────────────────────────────────────────── + +#[test] +fn clawback_proof_verifies() { + // Issuer encrypts a "balance" under their own pubkey (this models the + // issuer's mirror of a holder's balance). + let (issuer_sk, issuer_pk) = keypair::generate().unwrap(); + let r = encrypt::random_blinding_factor().unwrap(); + let amount = 500u64; + let mirror = encrypt::encrypt(amount, &issuer_pk, &r).unwrap(); + + let acc = AccountId::new([10; 20]); + let iss = IssuanceId::new([20; 24]); + let holder = AccountId::new([30; 20]); + let ctx = context::clawback(&acc, &iss, 7, &holder).unwrap(); + + let proof = prove::clawback(&issuer_sk, &issuer_pk, &ctx, amount, &mirror).unwrap(); + + verify::clawback(&proof, amount, &issuer_pk, &mirror, &ctx) + .expect("clawback proof failed verification"); +} + +// ───────────────────────────────────────────────────────────────────────── +// ConvertBack proof — round-trip via the safe verifier +// ───────────────────────────────────────────────────────────────────────── + +/// Happy-path round trip for `ConfidentialMPTConvertBack` (XLS-0096 §10). +/// +/// A holder reveals a withdrawal amount `m` and converts that much of their +/// confidential balance back to public. The 816-byte proof simultaneously +/// establishes three things: +/// +/// 1. **Ownership** — holder knows the sk for `HolderEncryptionKey`. +/// (compact sigma, 128 B) +/// 2. **Commitment linkage** — `BalanceCommitment` represents the same +/// balance as the on-ledger CB_S ciphertext. +/// (same compact sigma; the holder's sk is the link) +/// 3. **Non-negative remainder** — `balance − m ≥ 0`, i.e. no overdraft. +/// (Bulletproof range proof, 688 B) +/// +/// This test covers only the **positive case**: consistent inputs → valid +/// proof → verifier accepts. It exists to catch FFI-layer breakage — wrong +/// sizes, header drift, parameter-ordering bugs, hash-domain typos. The +/// rejection path (overdraft) lives in +/// [`convert_back_proof_rejects_withdrawal_exceeding_balance`]. +/// +/// ## Why two independent blinding factors +/// `balance_ciphertext_randomness` is the ElGamal `r` used when encrypting +/// the on-ledger balance ciphertext. `balance_blinding` is the Pedersen `ρ` +/// used in the commitment. They are independent scalars — the proof links +/// the two views of the balance via the holder's sk without requiring +/// `r == ρ`. (Contrast with Send, where a shared `r` IS reused as the +/// Pedersen blinding for `AmountCommitment` per §5.4.) +#[test] +fn convert_back_proof_verifies() { + let (holder_privkey, holder_pubkey) = keypair::generate().unwrap(); + + let current_balance = 1_000u64; + let withdraw_amount = 250u64; + + // The holder's on-ledger `ConfidentialBalanceSpending` (CB_S): + // ElGamal encryption of `current_balance` under `holder_pubkey` with + // fresh randomness. + let balance_ciphertext_randomness = encrypt::random_blinding_factor().unwrap(); + let balance_ciphertext = encrypt::encrypt( + current_balance, + &holder_pubkey, + &balance_ciphertext_randomness, + ) + .unwrap(); + + // Pedersen commitment to the same balance, but with an INDEPENDENT + // blinding (conventionally written ρ; unrelated to the ElGamal `r` + // above). The proof binds these two views of the balance via the + // sender's secret key — see XLS-0096 §10 / §5.4. + let balance_blinding = encrypt::random_blinding_factor().unwrap(); + let balance_commitment = commit::pedersen(current_balance, &balance_blinding).unwrap(); + + let holder_account = AccountId::new([5; 20]); + let issuance_id = IssuanceId::new([6; 24]); + let context_hash = context::convert_back( + &holder_account, + &issuance_id, + /* sequence */ 1, + /* version */ 0, + ) + .unwrap(); + + let proof = prove::convert_back(prove::ConvertBackProofParams { + holder_privkey: &holder_privkey, + holder_pubkey: &holder_pubkey, + amount: withdraw_amount, + current_balance, + context_hash: &context_hash, + balance_commitment: &balance_commitment, + balance_blinding: &balance_blinding, + balance_ciphertext: &balance_ciphertext, + }) + .unwrap(); + + verify::convert_back(verify::ConvertBackVerifyParams { + proof: &proof, + holder_pubkey: &holder_pubkey, + balance_ciphertext: &balance_ciphertext, + balance_commitment: &balance_commitment, + amount: withdraw_amount, + context_hash: &context_hash, + }) + .expect("convert_back proof failed verification"); +} + +/// Negative-path counterpart to [`convert_back_proof_verifies`]. +/// +/// Exercises the security property of the embedded Bulletproof range proof: +/// a holder cannot prove that withdrawing more than they hold leaves a +/// non-negative remainder. In the secp256k1 scalar field, `balance − m` +/// when `m > balance` wraps to a value near the group order — far outside +/// the [0, 2^64) range the Bulletproof claims to cover. +/// +/// The proof system can refuse the bad witness in either of two places: +/// 1. **Prover-side refusal.** The Bulletproof prover detects the out-of- +/// range remainder during bit decomposition and returns an error +/// directly — `prove::convert_back` returns `Err(_)`. +/// 2. **Verifier-side rejection.** The prover produces some proof bytes +/// anyway; the verifier rejects them when checking the range proof. +/// +/// Either path satisfies the security requirement (no on-chain ConvertBack +/// can drain a confidential balance below zero), so this test asserts that +/// at least one of them fires. +#[test] +fn convert_back_proof_rejects_withdrawal_exceeding_balance() { + let (holder_privkey, holder_pubkey) = keypair::generate().unwrap(); + + let current_balance = 100u64; + let withdraw_amount = 200u64; // strictly greater than current_balance + + let balance_ciphertext_randomness = encrypt::random_blinding_factor().unwrap(); + let balance_ciphertext = encrypt::encrypt( + current_balance, + &holder_pubkey, + &balance_ciphertext_randomness, + ) + .unwrap(); + + let balance_blinding = encrypt::random_blinding_factor().unwrap(); + let balance_commitment = commit::pedersen(current_balance, &balance_blinding).unwrap(); + + let holder_account = AccountId::new([5; 20]); + let issuance_id = IssuanceId::new([6; 24]); + let context_hash = context::convert_back( + &holder_account, + &issuance_id, + /* sequence */ 1, + /* version */ 0, + ) + .unwrap(); + + let prove_result = prove::convert_back(prove::ConvertBackProofParams { + holder_privkey: &holder_privkey, + holder_pubkey: &holder_pubkey, + amount: withdraw_amount, + current_balance, + context_hash: &context_hash, + balance_commitment: &balance_commitment, + balance_blinding: &balance_blinding, + balance_ciphertext: &balance_ciphertext, + }); + + let proof = match prove_result { + Err(_) => { + // Path 1: prover refused. Done — security property upheld. + return; + } + Ok(p) => p, + }; + + // Path 2: prover produced bytes despite the inconsistent witness. + // The verifier MUST reject them. + assert!( + verify::convert_back(verify::ConvertBackVerifyParams { + proof: &proof, + holder_pubkey: &holder_pubkey, + balance_ciphertext: &balance_ciphertext, + balance_commitment: &balance_commitment, + amount: withdraw_amount, + context_hash: &context_hash, + }) + .is_err(), + "verifier accepted a ConvertBack proof where withdraw > balance" + ); +} + +// ───────────────────────────────────────────────────────────────────────── +// Send proof — round-trip via the safe verifier (3 participants, no auditor) +// ───────────────────────────────────────────────────────────────────────── + +/// Builds a consistent 3-participant Send scenario and returns everything a +/// verifier needs. Shared by the Send and range-proof tests. +struct SendScenario { + proof: SendProof, + sender_pk: Pubkey, + recv_pk: Pubkey, + iss_pk: Pubkey, + sender_amount_ct: Ciphertext, + recv_amount_ct: Ciphertext, + iss_amount_ct: Ciphertext, + sender_balance_ct: Ciphertext, + amount_commitment: Commitment, + balance_commitment: Commitment, + ctx: ContextHash, +} + +fn build_send_scenario() -> SendScenario { + let (sender_sk, sender_pk) = keypair::generate().unwrap(); + let (_recv_sk, recv_pk) = keypair::generate().unwrap(); + let (_iss_sk, iss_pk) = keypair::generate().unwrap(); + + let balance = 1_000u64; + let amount = 400u64; + + // Sender's on-ledger CB_S. + let r_balance = encrypt::random_blinding_factor().unwrap(); + let sender_balance_ct = encrypt::encrypt(balance, &sender_pk, &r_balance).unwrap(); + + // Pedersen commitment to balance. + let rho_balance = encrypt::random_blinding_factor().unwrap(); + let balance_commitment = commit::pedersen(balance, &rho_balance).unwrap(); + + // Shared `r` across all three recipient ciphertexts AND the amount commitment. + let tx_r = encrypt::random_blinding_factor().unwrap(); + let sender_amount_ct = encrypt::encrypt(amount, &sender_pk, &tx_r).unwrap(); + let recv_amount_ct = encrypt::encrypt(amount, &recv_pk, &tx_r).unwrap(); + let iss_amount_ct = encrypt::encrypt(amount, &iss_pk, &tx_r).unwrap(); + let amount_commitment = commit::pedersen(amount, &tx_r).unwrap(); + + let snd_addr = AccountId::new([1; 20]); + let dst_addr = AccountId::new([2; 20]); + let iss_id = IssuanceId::new([3; 24]); + let ctx = context::send(&snd_addr, &iss_id, 1, &dst_addr, 0).unwrap(); + + let proof = prove::send(prove::SendProofParams { + sender_privkey: &sender_sk, + sender_pubkey: &sender_pk, + amount, + current_balance: balance, + tx_blinding_factor: &tx_r, + context_hash: &ctx, + amount_commitment: &amount_commitment, + balance_commitment: &balance_commitment, + balance_blinding: &rho_balance, + balance_ciphertext: &sender_balance_ct, + sender: prove::Participant { + pubkey: &sender_pk, + ciphertext: &sender_amount_ct, + }, + destination: prove::Participant { + pubkey: &recv_pk, + ciphertext: &recv_amount_ct, + }, + issuer: prove::Participant { + pubkey: &iss_pk, + ciphertext: &iss_amount_ct, + }, + auditor: None, + }) + .unwrap(); + + SendScenario { + proof, + sender_pk, + recv_pk, + iss_pk, + sender_amount_ct, + recv_amount_ct, + iss_amount_ct, + sender_balance_ct, + amount_commitment, + balance_commitment, + ctx, + } +} + +#[test] +fn send_proof_verifies_three_participants() { + let s = build_send_scenario(); + + verify::send(verify::SendVerifyParams { + proof: &s.proof, + sender: prove::Participant { + pubkey: &s.sender_pk, + ciphertext: &s.sender_amount_ct, + }, + destination: prove::Participant { + pubkey: &s.recv_pk, + ciphertext: &s.recv_amount_ct, + }, + issuer: prove::Participant { + pubkey: &s.iss_pk, + ciphertext: &s.iss_amount_ct, + }, + auditor: None, + sender_spending_ciphertext: &s.sender_balance_ct, + amount_commitment: &s.amount_commitment, + balance_commitment: &s.balance_commitment, + context_hash: &s.ctx, + }) + .expect("send proof failed verification"); +} + +/// The 754-byte range component sits after the 192-byte compact sigma inside +/// the 946-byte Send proof. Verifying that slice on its own exercises the +/// lower-level [`verify::send_range_proof`] wrapper against the C oracle. +#[test] +fn send_range_proof_verifies_extracted_subproof() { + let s = build_send_scenario(); + let range = &s.proof.as_bytes()[192..]; // 754-byte double Bulletproof + + verify::send_range_proof(range, &s.amount_commitment, &s.balance_commitment, &s.ctx) + .expect("extracted send range proof failed verification"); +} + +// ───────────────────────────────────────────────────────────────────────── +// Revealed-amount consistency +// ───────────────────────────────────────────────────────────────────────── + +#[test] +fn revealed_amount_verifies_matching_ciphertexts() { + let (_h_sk, holder_pk) = keypair::generate().unwrap(); + let (_i_sk, issuer_pk) = keypair::generate().unwrap(); + let (_a_sk, auditor_pk) = keypair::generate().unwrap(); + + let amount = 777u64; + // The SAME ElGamal randomness `r` is used for every participant's + // ciphertext — that shared scalar is what `revealed_amount` checks. + let r = encrypt::random_blinding_factor().unwrap(); + let holder_ct = encrypt::encrypt(amount, &holder_pk, &r).unwrap(); + let issuer_ct = encrypt::encrypt(amount, &issuer_pk, &r).unwrap(); + let auditor_ct = encrypt::encrypt(amount, &auditor_pk, &r).unwrap(); + + let holder = prove::Participant { + pubkey: &holder_pk, + ciphertext: &holder_ct, + }; + let issuer = prove::Participant { + pubkey: &issuer_pk, + ciphertext: &issuer_ct, + }; + let auditor = prove::Participant { + pubkey: &auditor_pk, + ciphertext: &auditor_ct, + }; + + verify::revealed_amount(amount, &r, holder, issuer, Some(auditor)) + .expect("matching ciphertexts should verify"); + + // A different claimed amount must be rejected. + assert!( + verify::revealed_amount(amount + 1, &r, holder, issuer, Some(auditor)).is_err(), + "verifier accepted a wrong revealed amount" + ); +} + +// ───────────────────────────────────────────────────────────────────────── +// Input-validation guards on the lower-level verifiers +// ───────────────────────────────────────────────────────────────────────── + +#[test] +fn send_range_proof_rejects_wrong_length() { + let r = encrypt::random_blinding_factor().unwrap(); + let c = commit::pedersen(1, &r).unwrap(); + let ctx = context::send( + &AccountId::new([1; 20]), + &IssuanceId::new([2; 24]), + 1, + &AccountId::new([3; 20]), + 0, + ) + .unwrap(); + + let err = verify::send_range_proof(&[0u8; 10], &c, &c, &ctx).unwrap_err(); + assert!( + matches!(err, Error::Invariant(_)), + "expected an Invariant error for a wrong-length range proof, got {err:?}" + ); +} + +#[test] +fn aggregated_bulletproof_rejects_empty_commitments() { + let ctx = context::convert(&AccountId::new([1; 20]), &IssuanceId::new([2; 24]), 1).unwrap(); + let err = verify::aggregated_bulletproof(&[0u8; 100], &[], &ctx).unwrap_err(); + assert!( + matches!(err, Error::Invariant(_)), + "expected an Invariant error for empty commitments, got {err:?}" + ); +} + +// ═════════════════════════════════════════════════════════════════════════ +// Negative / edge cases mirrored from upstream tests/test_mpt_utility.cpp +// +// Upstream exercises, for each proof type: a corrupted proof (first byte +// XORed), a zeroed context hash, and wrong public inputs. We reproduce those +// against the safe `verify` API so a regression in our FFI plumbing (wrong +// buffer size, parameter order, ctx binding) surfaces as a test failure. +// +// Upstream's `n=2` prover/verifier rejections have no analogue here: the +// `SendProofParams`/`SendVerifyParams` types require sender + destination + +// issuer, so fewer than three participants cannot be expressed. +// ═════════════════════════════════════════════════════════════════════════ + +/// Flip the first byte of any proof's bytes — the upstream "corrupted proof" +/// mutation (`proof[0] ^= 0xFF`). +fn flip_first_byte(bytes: &[u8; N]) -> [u8; N] { + let mut out = *bytes; + out[0] ^= 0xFF; + out +} + +const ZERO_CTX: ContextHash = ContextHash::new([0u8; 32]); + +// ── Encrypt/decrypt edge amounts (upstream: 0, 1, 1000) ────────────────── + +#[test] +fn encrypt_decrypt_edge_amounts() { + let (sk, pk) = keypair::generate().unwrap(); + for amount in [0u64, 1, 1000] { + let r = encrypt::random_blinding_factor().unwrap(); + let ct = encrypt::encrypt(amount, &pk, &r).unwrap(); + assert_eq!( + encrypt::decrypt(&ct, &sk).unwrap(), + amount, + "round-trip failed for amount {amount}" + ); + } +} + +// ── Convert negatives ──────────────────────────────────────────────────── + +#[test] +fn convert_proof_rejects_corrupted_proof() { + let (sk, pk) = keypair::generate().unwrap(); + let ctx = context::convert(&AccountId::new([1; 20]), &IssuanceId::new([2; 24]), 1).unwrap(); + let proof = prove::convert(&sk, &pk, &ctx).unwrap(); + + let corrupted = ConvertProof::new(flip_first_byte(proof.as_bytes())); + assert!( + verify::convert(&corrupted, &pk, &ctx).is_err(), + "verifier accepted a corrupted convert proof" + ); +} + +#[test] +fn convert_proof_rejects_wrong_context_hash() { + let (sk, pk) = keypair::generate().unwrap(); + let ctx = context::convert(&AccountId::new([1; 20]), &IssuanceId::new([2; 24]), 1).unwrap(); + let proof = prove::convert(&sk, &pk, &ctx).unwrap(); + + assert!( + verify::convert(&proof, &pk, &ZERO_CTX).is_err(), + "verifier accepted a convert proof against a zeroed context hash" + ); +} + +// ── Send: n=4 (with auditor) + negatives ───────────────────────────────── + +#[test] +fn send_proof_verifies_four_participants_with_auditor() { + let (sender_sk, sender_pk) = keypair::generate().unwrap(); + let (_recv_sk, recv_pk) = keypair::generate().unwrap(); + let (_iss_sk, iss_pk) = keypair::generate().unwrap(); + let (_aud_sk, aud_pk) = keypair::generate().unwrap(); + + let balance = 1_000u64; + let amount = 400u64; + + let r_balance = encrypt::random_blinding_factor().unwrap(); + let sender_balance_ct = encrypt::encrypt(balance, &sender_pk, &r_balance).unwrap(); + let rho_balance = encrypt::random_blinding_factor().unwrap(); + let balance_commitment = commit::pedersen(balance, &rho_balance).unwrap(); + + let tx_r = encrypt::random_blinding_factor().unwrap(); + let sender_amount_ct = encrypt::encrypt(amount, &sender_pk, &tx_r).unwrap(); + let recv_amount_ct = encrypt::encrypt(amount, &recv_pk, &tx_r).unwrap(); + let iss_amount_ct = encrypt::encrypt(amount, &iss_pk, &tx_r).unwrap(); + let aud_amount_ct = encrypt::encrypt(amount, &aud_pk, &tx_r).unwrap(); + let amount_commitment = commit::pedersen(amount, &tx_r).unwrap(); + + let ctx = context::send( + &AccountId::new([1; 20]), + &IssuanceId::new([3; 24]), + 1, + &AccountId::new([2; 20]), + 0, + ) + .unwrap(); + + let sender = prove::Participant { + pubkey: &sender_pk, + ciphertext: &sender_amount_ct, + }; + let destination = prove::Participant { + pubkey: &recv_pk, + ciphertext: &recv_amount_ct, + }; + let issuer = prove::Participant { + pubkey: &iss_pk, + ciphertext: &iss_amount_ct, + }; + let auditor = prove::Participant { + pubkey: &aud_pk, + ciphertext: &aud_amount_ct, + }; + + let proof = prove::send(prove::SendProofParams { + sender_privkey: &sender_sk, + sender_pubkey: &sender_pk, + amount, + current_balance: balance, + tx_blinding_factor: &tx_r, + context_hash: &ctx, + amount_commitment: &amount_commitment, + balance_commitment: &balance_commitment, + balance_blinding: &rho_balance, + balance_ciphertext: &sender_balance_ct, + sender, + destination, + issuer, + auditor: Some(auditor), + }) + .unwrap(); + + verify::send(verify::SendVerifyParams { + proof: &proof, + sender, + destination, + issuer, + auditor: Some(auditor), + sender_spending_ciphertext: &sender_balance_ct, + amount_commitment: &amount_commitment, + balance_commitment: &balance_commitment, + context_hash: &ctx, + }) + .expect("send proof (n=4, with auditor) failed verification"); +} + +/// Build the all-correct `SendVerifyParams` for a 3-participant scenario. +/// Individual negative tests then override one field. +fn send_verify_params<'a>(s: &'a SendScenario) -> verify::SendVerifyParams<'a> { + verify::SendVerifyParams { + proof: &s.proof, + sender: prove::Participant { + pubkey: &s.sender_pk, + ciphertext: &s.sender_amount_ct, + }, + destination: prove::Participant { + pubkey: &s.recv_pk, + ciphertext: &s.recv_amount_ct, + }, + issuer: prove::Participant { + pubkey: &s.iss_pk, + ciphertext: &s.iss_amount_ct, + }, + auditor: None, + sender_spending_ciphertext: &s.sender_balance_ct, + amount_commitment: &s.amount_commitment, + balance_commitment: &s.balance_commitment, + context_hash: &s.ctx, + } +} + +#[test] +fn send_proof_rejects_corrupted_proof() { + let s = build_send_scenario(); + let corrupted = SendProof::new(flip_first_byte(s.proof.as_bytes())); + let mut params = send_verify_params(&s); + params.proof = &corrupted; + assert!( + verify::send(params).is_err(), + "verifier accepted a corrupted send proof" + ); +} + +#[test] +fn send_proof_rejects_wrong_context_hash() { + let s = build_send_scenario(); + let mut params = send_verify_params(&s); + params.context_hash = &ZERO_CTX; + assert!( + verify::send(params).is_err(), + "verifier accepted a send proof against a zeroed context hash" + ); +} + +#[test] +fn send_proof_rejects_wrong_amount_commitment() { + let s = build_send_scenario(); + let wrong = commit::pedersen(123_456, &encrypt::random_blinding_factor().unwrap()).unwrap(); + let mut params = send_verify_params(&s); + params.amount_commitment = &wrong; + assert!( + verify::send(params).is_err(), + "verifier accepted a send proof with a wrong amount commitment" + ); +} + +#[test] +fn send_proof_rejects_wrong_balance_commitment() { + let s = build_send_scenario(); + let wrong = commit::pedersen(123_456, &encrypt::random_blinding_factor().unwrap()).unwrap(); + let mut params = send_verify_params(&s); + params.balance_commitment = &wrong; + assert!( + verify::send(params).is_err(), + "verifier accepted a send proof with a wrong balance commitment" + ); +} + +#[test] +fn send_proof_rejects_wrong_balance_ciphertext() { + let s = build_send_scenario(); + let wrong = encrypt::encrypt( + 999, + &s.sender_pk, + &encrypt::random_blinding_factor().unwrap(), + ) + .unwrap(); + let mut params = send_verify_params(&s); + params.sender_spending_ciphertext = &wrong; + assert!( + verify::send(params).is_err(), + "verifier accepted a send proof with a wrong spending-balance ciphertext" + ); +} + +// ── ConvertBack negatives ───────────────────────────────────────────────── + +struct ConvertBackScenario { + proof: ConvertBackProof, + holder_pubkey: Pubkey, + balance_ciphertext: Ciphertext, + balance_commitment: Commitment, + withdraw_amount: u64, + ctx: ContextHash, +} + +fn build_convert_back_scenario() -> ConvertBackScenario { + let (holder_privkey, holder_pubkey) = keypair::generate().unwrap(); + let current_balance = 5_000u64; + let withdraw_amount = 1_000u64; + + let r = encrypt::random_blinding_factor().unwrap(); + let balance_ciphertext = encrypt::encrypt(current_balance, &holder_pubkey, &r).unwrap(); + let rho = encrypt::random_blinding_factor().unwrap(); + let balance_commitment = commit::pedersen(current_balance, &rho).unwrap(); + let ctx = + context::convert_back(&AccountId::new([5; 20]), &IssuanceId::new([6; 24]), 1, 0).unwrap(); + + let proof = prove::convert_back(prove::ConvertBackProofParams { + holder_privkey: &holder_privkey, + holder_pubkey: &holder_pubkey, + amount: withdraw_amount, + current_balance, + context_hash: &ctx, + balance_commitment: &balance_commitment, + balance_blinding: &rho, + balance_ciphertext: &balance_ciphertext, + }) + .unwrap(); + + ConvertBackScenario { + proof, + holder_pubkey, + balance_ciphertext, + balance_commitment, + withdraw_amount, + ctx, + } +} + +fn convert_back_verify_params<'a>( + s: &'a ConvertBackScenario, +) -> verify::ConvertBackVerifyParams<'a> { + verify::ConvertBackVerifyParams { + proof: &s.proof, + holder_pubkey: &s.holder_pubkey, + balance_ciphertext: &s.balance_ciphertext, + balance_commitment: &s.balance_commitment, + amount: s.withdraw_amount, + context_hash: &s.ctx, + } +} + +#[test] +fn convert_back_proof_rejects_corrupted_proof() { + let s = build_convert_back_scenario(); + let corrupted = ConvertBackProof::new(flip_first_byte(s.proof.as_bytes())); + let mut params = convert_back_verify_params(&s); + params.proof = &corrupted; + assert!( + verify::convert_back(params).is_err(), + "verifier accepted a corrupted convert_back proof" + ); +} + +#[test] +fn convert_back_proof_rejects_wrong_context_hash() { + let s = build_convert_back_scenario(); + let mut params = convert_back_verify_params(&s); + params.context_hash = &ZERO_CTX; + assert!( + verify::convert_back(params).is_err(), + "verifier accepted a convert_back proof against a zeroed context hash" + ); +} + +#[test] +fn convert_back_proof_rejects_wrong_balance_commitment() { + let s = build_convert_back_scenario(); + let wrong = commit::pedersen(123_456, &encrypt::random_blinding_factor().unwrap()).unwrap(); + let mut params = convert_back_verify_params(&s); + params.balance_commitment = &wrong; + assert!( + verify::convert_back(params).is_err(), + "verifier accepted a convert_back proof with a wrong balance commitment" + ); +} + +#[test] +fn convert_back_proof_rejects_wrong_balance_ciphertext() { + let s = build_convert_back_scenario(); + let wrong = encrypt::encrypt( + 999, + &s.holder_pubkey, + &encrypt::random_blinding_factor().unwrap(), + ) + .unwrap(); + let mut params = convert_back_verify_params(&s); + params.balance_ciphertext = &wrong; + assert!( + verify::convert_back(params).is_err(), + "verifier accepted a convert_back proof with a wrong balance ciphertext" + ); +} + +// ── Clawback negatives ───────────────────────────────────────────────────── + +struct ClawbackScenario { + proof: ClawbackProof, + amount: u64, + issuer_pk: Pubkey, + mirror: Ciphertext, + ctx: ContextHash, +} + +fn build_clawback_scenario() -> ClawbackScenario { + let (issuer_sk, issuer_pk) = keypair::generate().unwrap(); + let r = encrypt::random_blinding_factor().unwrap(); + let amount = 500u64; + let mirror = encrypt::encrypt(amount, &issuer_pk, &r).unwrap(); + let ctx = context::clawback( + &AccountId::new([10; 20]), + &IssuanceId::new([20; 24]), + 7, + &AccountId::new([30; 20]), + ) + .unwrap(); + let proof = prove::clawback(&issuer_sk, &issuer_pk, &ctx, amount, &mirror).unwrap(); + + ClawbackScenario { + proof, + amount, + issuer_pk, + mirror, + ctx, + } +} + +#[test] +fn clawback_proof_rejects_corrupted_proof() { + let s = build_clawback_scenario(); + let corrupted = ClawbackProof::new(flip_first_byte(s.proof.as_bytes())); + assert!( + verify::clawback(&corrupted, s.amount, &s.issuer_pk, &s.mirror, &s.ctx).is_err(), + "verifier accepted a corrupted clawback proof" + ); +} + +#[test] +fn clawback_proof_rejects_wrong_context_hash() { + let s = build_clawback_scenario(); + assert!( + verify::clawback(&s.proof, s.amount, &s.issuer_pk, &s.mirror, &ZERO_CTX).is_err(), + "verifier accepted a clawback proof against a zeroed context hash" + ); +} + +#[test] +fn clawback_proof_rejects_wrong_amount() { + let s = build_clawback_scenario(); + // Upstream verifies 500-byte proof against amount 999. + assert!( + verify::clawback(&s.proof, s.amount + 499, &s.issuer_pk, &s.mirror, &s.ctx).is_err(), + "verifier accepted a clawback proof against the wrong amount" + ); +} + +#[test] +fn clawback_proof_rejects_wrong_ciphertext() { + let s = build_clawback_scenario(); + // Same amount, but a ciphertext made with a different blinding factor. + let wrong = encrypt::encrypt( + s.amount, + &s.issuer_pk, + &encrypt::random_blinding_factor().unwrap(), + ) + .unwrap(); + assert!( + verify::clawback(&s.proof, s.amount, &s.issuer_pk, &wrong, &s.ctx).is_err(), + "verifier accepted a clawback proof against a wrong ciphertext" + ); +} diff --git a/src/asynch/transaction/mod.rs b/src/asynch/transaction/mod.rs index d7e38a11..32b2edf5 100644 --- a/src/asynch/transaction/mod.rs +++ b/src/asynch/transaction/mod.rs @@ -42,6 +42,11 @@ const OWNER_RESERVE: &str = "2000000"; // 2 XRP const RESTRICTED_NETWORKS: u16 = 1024; const REQUIRED_NETWORKID_VERSION: &str = "1.11.0"; const LEDGER_OFFSET: u8 = 20; +/// ConfidentialMPT transactions pay 10× the reference base fee, covering the +/// cost of verifying the transaction's zero-knowledge proof. Mirrors rippled's +/// cMPT transactors (`Transactor::calculateBaseFee + base × +/// kCONFIDENTIAL_FEE_MULTIPLIER`), whose total is `10 × base`. +const CONFIDENTIAL_MPT_FEE_MULTIPLIER: u64 = 10; pub async fn sign_and_submit<'a, 'b, T, F, C>( transaction: &mut T, @@ -156,6 +161,13 @@ where TransactionType::AMMCreate | TransactionType::AccountDelete => { get_owner_reserve_from_response(client).await? } + TransactionType::ConfidentialMPTConvert + | TransactionType::ConfidentialMPTConvertBack + | TransactionType::ConfidentialMPTSend + | TransactionType::ConfidentialMPTClawback + | TransactionType::ConfidentialMPTMergeInbox => { + calculate_base_fee_for_confidential_mpt(net_fee.clone())? + } _ => net_fee.clone(), }; } else { @@ -169,6 +181,13 @@ where TransactionType::AMMCreate | TransactionType::AccountDelete => { XRPAmount::from(OWNER_RESERVE) } + TransactionType::ConfidentialMPTConvert + | TransactionType::ConfidentialMPTConvertBack + | TransactionType::ConfidentialMPTSend + | TransactionType::ConfidentialMPTClawback + | TransactionType::ConfidentialMPTMergeInbox => { + calculate_base_fee_for_confidential_mpt(net_fee.clone())? + } _ => net_fee.clone(), }; } @@ -222,6 +241,19 @@ fn calculate_based_on_fulfillment<'a>( .into()) } +/// ConfidentialMPT transactions pay [`CONFIDENTIAL_MPT_FEE_MULTIPLIER`]× the +/// reference base fee (rippled's `kCONFIDENTIAL_FEE_MULTIPLIER`), reflecting the +/// cost of verifying the transaction's zero-knowledge proof. +fn calculate_base_fee_for_confidential_mpt<'a: 'b, 'b>( + net_fee: XRPAmount<'a>, +) -> XRPLHelperResult> { + let net_fee_decimal: BigDecimal = net_fee.try_into()?; + let base_fee_decimal = net_fee_decimal * BigDecimal::from(CONFIDENTIAL_MPT_FEE_MULTIPLIER); + Ok(base_fee_decimal + .with_scale_round(0, RoundingMode::Down) + .into()) +} + fn txn_needs_network_id(common_fields: CommonFields<'_>) -> XRPLHelperResult { let is_higher_restricted_networks = if let Some(network_id) = common_fields.network_id { network_id > RESTRICTED_NETWORKS as u32 @@ -516,3 +548,82 @@ mod test_sign { assert!(!common_fields.is_signed()); // Should not be signed yet } } + +#[cfg(all(feature = "json-rpc", feature = "std"))] +#[cfg(test)] +mod test_calculate_fee { + use super::calculate_base_fee_for_confidential_mpt; + use crate::models::XRPAmount; + + #[test] + fn confidential_mpt_base_fee_is_ten_times_base() { + let fee = calculate_base_fee_for_confidential_mpt(XRPAmount::from("200")).unwrap(); + assert_eq!(fee, XRPAmount::from("2000")); + } +} + +// Autofill flow for every ConfidentialMPT transaction (akin to `test_autofill`'s +// OfferCreate test). These tests use `AsyncJsonRpcClient` against `localhost:5005`. They +// assert autofill populates the common fields and applies the 10× cMPT fee. +#[cfg(all( + feature = "json-rpc", + feature = "std", + feature = "integration", + feature = "confidential-mpt" +))] +#[cfg(test)] +mod test_autofill_confidential_mpt { + use super::autofill; + use crate::asynch::clients::AsyncJsonRpcClient; + use crate::models::transactions::{ + confidential_mpt_clawback::ConfidentialMPTClawback, + confidential_mpt_convert::ConfidentialMPTConvert, + confidential_mpt_convert_back::ConfidentialMPTConvertBack, + confidential_mpt_merge_inbox::ConfidentialMPTMergeInbox, + confidential_mpt_send::ConfidentialMPTSend, CommonFields, Transaction, TransactionType, + }; + use crate::models::XRPAmount; + + // Genesis account on the standalone node — exists + funded, so autofill can + // resolve its sequence. autofill never submits, so each transaction's + // cMPT-specific fields are left at their defaults. + const GENESIS: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + const STANDALONE_URL: &str = "http://localhost:5005"; + + // Each ConfidentialMPT transaction autofills to 10× the reference base fee + // (rippled's kCONFIDENTIAL_FEE_MULTIPLIER); the node's 200-drop base → 2000, + // alongside an autofilled sequence + last_ledger_sequence. + macro_rules! autofill_fee_test { + ($name:ident, $ty:ident) => { + #[tokio::test] + async fn $name() { + let mut tx = $ty { + common_fields: CommonFields { + account: GENESIS.into(), + transaction_type: TransactionType::$ty, + ..Default::default() + }, + ..Default::default() + }; + let client = AsyncJsonRpcClient::connect(STANDALONE_URL.parse().unwrap()); + autofill(&mut tx, &client, None) + .await + .expect("autofill should populate the common fields"); + + let common = tx.get_common_fields(); + assert!(common.sequence.is_some(), "autofill should set sequence"); + assert!( + common.last_ledger_sequence.is_some(), + "autofill should set last_ledger_sequence" + ); + assert_eq!(common.fee, Some(XRPAmount::from("2000"))); + } + }; + } + + autofill_fee_test!(merge_inbox_autofill, ConfidentialMPTMergeInbox); + autofill_fee_test!(convert_autofill, ConfidentialMPTConvert); + autofill_fee_test!(convert_back_autofill, ConfidentialMPTConvertBack); + autofill_fee_test!(send_autofill, ConfidentialMPTSend); + autofill_fee_test!(clawback_autofill, ConfidentialMPTClawback); +} diff --git a/src/core/binarycodec/definitions/definitions.json b/src/core/binarycodec/definitions/definitions.json index ccd7b799..b3167181 100644 --- a/src/core/binarycodec/definitions/definitions.json +++ b/src/core/binarycodec/definitions/definitions.json @@ -1,4 +1,22 @@ { + "ACCOUNT_SET_FLAGS": { + "asfAccountTxnID": 5, + "asfAllowTrustLineClawback": 16, + "asfAllowTrustLineLocking": 17, + "asfAuthorizedNFTokenMinter": 10, + "asfDefaultRipple": 8, + "asfDepositAuth": 9, + "asfDisableMaster": 4, + "asfDisallowIncomingCheck": 13, + "asfDisallowIncomingNFTokenOffer": 12, + "asfDisallowIncomingPayChan": 14, + "asfDisallowIncomingTrustline": 15, + "asfDisallowXRP": 3, + "asfGlobalFreeze": 7, + "asfNoFreeze": 6, + "asfRequireAuth": 2, + "asfRequireDest": 1 + }, "FIELDS": [ [ "Generic", @@ -61,3377 +79,5023 @@ } ], [ - "LedgerEntryType", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "UInt16" - } - ], - [ - "TransactionType", + "VoteEntry", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "UInt16" + "nth": 25, + "type": "STObject" } ], [ - "SignerWeight", + "HookGrant", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "UInt16" + "nth": 24, + "type": "STObject" } ], [ - "TransferFee", + "HookParameter", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "UInt16" + "nth": 23, + "type": "STObject" } ], [ - "TradingFee", + "HookDefinition", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "UInt16" + "nth": 22, + "type": "STObject" } ], [ - "DiscountedFee", + "HookExecution", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "UInt16" + "nth": 21, + "type": "STObject" } ], [ - "Version", + "EmittedTxn", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "UInt16" + "nth": 20, + "type": "STObject" } ], [ - "HookStateChangeCount", + "DisabledValidator", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 17, - "type": "UInt16" + "nth": 19, + "type": "STObject" } ], [ - "HookEmitCount", + "Majority", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 18, - "type": "UInt16" + "type": "STObject" } ], [ - "HookExecutionIndex", + "TemplateEntry", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, - "type": "UInt16" + "nth": 9, + "type": "STObject" } ], [ - "HookApiVersion", + "NewFields", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, - "type": "UInt16" + "nth": 8, + "type": "STObject" } ], [ - "LedgerFixType", + "FinalFields", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 21, - "type": "UInt16" + "nth": 7, + "type": "STObject" } ], [ - "ManagementFeeRate", + "PreviousFields", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, - "type": "UInt16" + "nth": 6, + "type": "STObject" } ], [ - "NetworkID", + "ModifiedNode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "UInt32" + "nth": 5, + "type": "STObject" } ], [ - "Flags", + "DeletedNode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "UInt32" + "nth": 4, + "type": "STObject" } ], [ - "SourceTag", + "CreatedNode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 3, - "type": "UInt32" + "type": "STObject" } ], [ - "Sequence", + "TransactionMetaData", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "UInt32" + "nth": 2, + "type": "STObject" } ], [ - "PreviousTxnLgrSeq", + "XChainBridge", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "UInt32" + "nth": 1, + "type": "XChainBridge" } ], [ - "LedgerSequence", + "Asset2", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "UInt32" + "nth": 4, + "type": "Issue" } ], [ - "CloseTime", + "Asset", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 7, - "type": "UInt32" + "nth": 3, + "type": "Issue" } ], [ - "ParentCloseTime", + "IssuingChainIssue", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 8, - "type": "UInt32" + "nth": 2, + "type": "Issue" } ], [ - "SigningTime", + "LockingChainIssue", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 9, - "type": "UInt32" + "nth": 1, + "type": "Issue" } ], [ - "Expiration", + "QuoteAsset", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 10, - "type": "UInt32" + "nth": 2, + "type": "Currency" } ], [ - "TransferRate", + "BaseAsset", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 11, - "type": "UInt32" + "nth": 1, + "type": "Currency" } ], [ - "WalletSize", + "Counterparty", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, - "type": "UInt32" + "isVLEncoded": true, + "nth": 26, + "type": "AccountID" } ], [ - "OwnerCount", + "Borrower", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 13, - "type": "UInt32" + "isVLEncoded": true, + "nth": 25, + "type": "AccountID" } ], [ - "DestinationTag", + "Subject", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 14, - "type": "UInt32" + "isVLEncoded": true, + "nth": 24, + "type": "AccountID" } ], [ - "LastUpdateTime", + "IssuingChainDoor", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 15, - "type": "UInt32" + "isVLEncoded": true, + "nth": 23, + "type": "AccountID" } ], [ - "HighQualityIn", + "LockingChainDoor", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, - "type": "UInt32" + "isVLEncoded": true, + "nth": 22, + "type": "AccountID" } ], [ - "HighQualityOut", + "AttestationRewardAccount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 17, - "type": "UInt32" + "isVLEncoded": true, + "nth": 21, + "type": "AccountID" } ], [ - "LowQualityIn", + "AttestationSignerAccount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, - "type": "UInt32" + "isVLEncoded": true, + "nth": 20, + "type": "AccountID" } ], [ - "LowQualityOut", + "OtherChainDestination", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, + "isVLEncoded": true, "nth": 19, - "type": "UInt32" + "type": "AccountID" } ], [ - "QualityIn", + "OtherChainSource", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 20, - "type": "UInt32" + "isVLEncoded": true, + "nth": 18, + "type": "AccountID" } ], [ - "QualityOut", + "HookAccount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, - "type": "UInt32" + "isVLEncoded": true, + "nth": 16, + "type": "AccountID" } ], [ - "StampEscrow", + "BalanceCommitment", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 22, - "type": "UInt32" + "isVLEncoded": true, + "nth": 46, + "type": "Blob" } ], [ - "BondAmount", + "AuditorEncryptionKey", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 23, - "type": "UInt32" + "isVLEncoded": true, + "nth": 44, + "type": "Blob" } ], [ - "LoadFee", + "Delegate", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 24, - "type": "UInt32" + "isVLEncoded": true, + "nth": 12, + "type": "AccountID" } ], [ - "OfferSequence", + "AuditorEncryptedAmount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, - "type": "UInt32" + "isVLEncoded": true, + "nth": 43, + "type": "Blob" } ], [ - "FirstLedgerSequence", + "Holder", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 26, - "type": "UInt32" + "isVLEncoded": true, + "nth": 11, + "type": "AccountID" } ], [ - "LastLedgerSequence", + "AuditorEncryptedBalance", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 27, - "type": "UInt32" + "isVLEncoded": true, + "nth": 42, + "type": "Blob" } ], [ - "TransactionIndex", + "Unauthorize", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 28, - "type": "UInt32" + "isVLEncoded": true, + "nth": 6, + "type": "AccountID" } ], [ - "OperationLimit", + "ZKProof", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 29, - "type": "UInt32" + "isVLEncoded": true, + "nth": 37, + "type": "Blob" } ], [ - "ReferenceFeeUnits", + "Authorize", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 30, - "type": "UInt32" + "isVLEncoded": true, + "nth": 5, + "type": "AccountID" } ], [ - "ReserveBase", + "HolderEncryptionKey", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 31, - "type": "UInt32" + "isVLEncoded": true, + "nth": 36, + "type": "Blob" } ], [ - "ReserveIncrement", + "Issuer", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 32, - "type": "UInt32" + "isVLEncoded": true, + "nth": 4, + "type": "AccountID" } ], [ - "SetFlag", + "IssuerEncryptionKey", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 33, - "type": "UInt32" + "isVLEncoded": true, + "nth": 35, + "type": "Blob" } ], [ - "ClearFlag", + "Destination", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 34, - "type": "UInt32" + "isVLEncoded": true, + "nth": 3, + "type": "AccountID" } ], [ - "SignerQuorum", + "IssuerEncryptedBalance", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 35, - "type": "UInt32" + "isVLEncoded": true, + "nth": 34, + "type": "Blob" } ], [ - "CancelAfter", + "Owner", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 36, - "type": "UInt32" + "isVLEncoded": true, + "nth": 2, + "type": "AccountID" } ], [ - "FinishAfter", + "ConfidentialBalanceSpending", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 37, - "type": "UInt32" + "isVLEncoded": true, + "nth": 33, + "type": "Blob" } ], [ - "SignerListID", + "Account", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 38, - "type": "UInt32" + "isVLEncoded": true, + "nth": 1, + "type": "AccountID" } ], [ - "SettleDelay", + "ConfidentialBalanceInbox", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 39, - "type": "UInt32" + "isVLEncoded": true, + "nth": 32, + "type": "Blob" } ], [ - "TicketCount", + "CredentialType", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 40, - "type": "UInt32" + "isVLEncoded": true, + "nth": 31, + "type": "Blob" } ], [ - "TicketSequence", + "MPTokenMetadata", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 41, - "type": "UInt32" + "isVLEncoded": true, + "nth": 30, + "type": "Blob" } ], [ - "NFTokenTaxon", + "Provider", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 42, - "type": "UInt32" + "isVLEncoded": true, + "nth": 29, + "type": "Blob" } ], [ - "MintedNFTokens", + "AssetClass", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 43, - "type": "UInt32" + "isVLEncoded": true, + "nth": 28, + "type": "Blob" } ], [ - "BurnedNFTokens", + "Data", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 44, - "type": "UInt32" + "isVLEncoded": true, + "nth": 27, + "type": "Blob" } ], [ - "HookStateCount", + "DIDDocument", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 45, - "type": "UInt32" + "isVLEncoded": true, + "nth": 26, + "type": "Blob" } ], [ - "EmitGeneration", + "HookParameterValue", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 46, - "type": "UInt32" + "isVLEncoded": true, + "nth": 25, + "type": "Blob" } ], [ - "VoteWeight", + "HookParameterName", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 48, - "type": "UInt32" + "isVLEncoded": true, + "nth": 24, + "type": "Blob" } ], [ - "FirstNFTokenSequence", + "HookReturnString", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 50, - "type": "UInt32" + "isVLEncoded": true, + "nth": 23, + "type": "Blob" } ], [ - "OracleDocumentID", + "HookStateData", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 51, - "type": "UInt32" + "isVLEncoded": true, + "nth": 22, + "type": "Blob" } ], [ - "PermissionValue", + "ValidatorToReEnable", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 52, - "type": "UInt32" + "isVLEncoded": true, + "nth": 21, + "type": "Blob" } ], [ - "MutableFlags", + "ValidatorToDisable", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 53, - "type": "UInt32" + "isVLEncoded": true, + "nth": 20, + "type": "Blob" } ], [ - "StartDate", + "UNLModifyValidator", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 54, - "type": "UInt32" + "isVLEncoded": true, + "nth": 19, + "type": "Blob" } ], [ - "PaymentInterval", + "MasterSignature", { "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 55, - "type": "UInt32" + "isSigningField": false, + "isVLEncoded": true, + "nth": 18, + "type": "Blob" } ], [ - "GracePeriod", + "Condition", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 56, - "type": "UInt32" + "isVLEncoded": true, + "nth": 17, + "type": "Blob" } ], [ - "PreviousPaymentDueDate", + "Fulfillment", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 57, - "type": "UInt32" + "isVLEncoded": true, + "nth": 16, + "type": "Blob" } ], [ - "NextPaymentDueDate", + "MemoData", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 58, - "type": "UInt32" + "isVLEncoded": true, + "nth": 13, + "type": "Blob" } ], [ - "PaymentRemaining", + "MemoType", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 59, - "type": "UInt32" + "isVLEncoded": true, + "nth": 12, + "type": "Blob" } ], [ - "PaymentTotal", + "CreateCode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 60, - "type": "UInt32" + "isVLEncoded": true, + "nth": 11, + "type": "Blob" } ], [ - "LoanSequence", + "ExpireCode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 61, - "type": "UInt32" + "isVLEncoded": true, + "nth": 10, + "type": "Blob" } ], [ - "CoverRateMinimum", + "RemoveCode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 62, - "type": "UInt32" + "isVLEncoded": true, + "nth": 9, + "type": "Blob" } ], [ - "CoverRateLiquidation", + "FundCode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 63, - "type": "UInt32" + "isVLEncoded": true, + "nth": 8, + "type": "Blob" } ], [ - "OverpaymentFee", + "Domain", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 64, - "type": "UInt32" + "isVLEncoded": true, + "nth": 7, + "type": "Blob" } ], [ - "InterestRate", + "Signature", { "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 65, - "type": "UInt32" + "isSigningField": false, + "isVLEncoded": true, + "nth": 6, + "type": "Blob" } ], [ - "LateInterestRate", + "URI", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 66, - "type": "UInt32" + "isVLEncoded": true, + "nth": 5, + "type": "Blob" } ], [ - "CloseInterestRate", + "TxnSignature", { "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 67, - "type": "UInt32" + "isSigningField": false, + "isVLEncoded": true, + "nth": 4, + "type": "Blob" } ], [ - "OverpaymentInterestRate", + "SigningPubKey", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 68, - "type": "UInt32" + "isVLEncoded": true, + "nth": 3, + "type": "Blob" } ], [ - "IndexNext", + "MessageKey", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "UInt64" + "isVLEncoded": true, + "nth": 2, + "type": "Blob" } ], [ - "IndexPrevious", + "PublicKey", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "UInt64" + "isVLEncoded": true, + "nth": 1, + "type": "Blob" } ], [ - "BookNode", + "LPTokenBalance", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "UInt64" + "nth": 31, + "type": "Amount" } ], [ - "OwnerNode", + "MinAccountCreateAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "UInt64" + "nth": 30, + "type": "Amount" } ], [ - "BaseFee", + "SignatureReward", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "UInt64" + "nth": 29, + "type": "Amount" } ], [ - "ExchangeRate", + "Price", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "UInt64" + "nth": 28, + "type": "Amount" } ], [ - "LowNode", + "EPrice", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 7, - "type": "UInt64" + "nth": 27, + "type": "Amount" } ], [ - "HighNode", + "LPTokenIn", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 8, - "type": "UInt64" + "nth": 26, + "type": "Amount" } ], [ - "DestinationNode", + "LPTokenOut", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 9, - "type": "UInt64" + "nth": 25, + "type": "Amount" } ], [ - "Cookie", + "ReserveIncrementDrops", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 10, - "type": "UInt64" + "nth": 24, + "type": "Amount" } ], [ - "ServerVersion", + "ReserveBaseDrops", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 11, - "type": "UInt64" + "nth": 23, + "type": "Amount" } ], [ - "NFTokenOfferNode", + "BaseFeeDrops", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 12, - "type": "UInt64" + "nth": 22, + "type": "Amount" } ], [ - "EmitBurden", + "NFTokenBrokerFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 13, - "type": "UInt64" + "nth": 19, + "type": "Amount" } ], [ - "HookOn", + "DeliveredAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "UInt64" + "nth": 18, + "type": "Amount" } ], [ - "HookInstructionCount", + "RippleEscrow", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 17, - "type": "UInt64" + "type": "Amount" } ], [ - "HookReturnCode", + "MinimumOffer", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 18, - "type": "UInt64" + "nth": 16, + "type": "Amount" } ], [ - "ReferenceCount", + "BidMax", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, - "type": "UInt64" + "nth": 13, + "type": "Amount" } ], [ - "XChainClaimID", + "BidMin", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, - "type": "UInt64" + "nth": 12, + "type": "Amount" } ], [ - "XChainAccountCreateCount", + "CredentialIDs", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, - "type": "UInt64" + "isVLEncoded": true, + "nth": 5, + "type": "Vector256" } ], [ - "XChainAccountClaimCount", + "Amount2", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, - "type": "UInt64" + "nth": 11, + "type": "Amount" } ], [ - "AssetPrice", + "NFTokenOffers", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 23, - "type": "UInt64" + "isVLEncoded": true, + "nth": 4, + "type": "Vector256" } ], [ - "MaximumAmount", + "DeliverMin", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 24, - "type": "UInt64" + "nth": 10, + "type": "Amount" } ], [ - "OutstandingAmount", + "Amendments", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, - "type": "UInt64" + "isVLEncoded": true, + "nth": 3, + "type": "Vector256" } ], [ - "MPTAmount", + "SendMax", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 26, - "type": "UInt64" + "nth": 9, + "type": "Amount" } ], [ - "IssuerNode", + "LoanScale", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 27, - "type": "UInt64" + "nth": 1, + "type": "Int32" } ], [ - "SubjectNode", + "LatePaymentFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 28, - "type": "UInt64" + "nth": 11, + "type": "Number" } ], [ - "LockedAmount", + "NFTokenMinter", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 29, - "type": "UInt64" + "isVLEncoded": true, + "nth": 9, + "type": "AccountID" } ], [ - "VaultNode", + "SenderEncryptedAmount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 30, - "type": "UInt64" + "isVLEncoded": true, + "nth": 40, + "type": "Blob" } ], [ - "LoanBrokerNode", + "TakerPaysMPT", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 31, - "type": "UInt64" + "nth": 3, + "type": "Hash192" } ], [ - "EmailHash", + "LoanServiceFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "Hash128" - } + "nth": 10, + "type": "Number" + } ], [ - "LedgerHash", + "RegularKey", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "Hash256" + "isVLEncoded": true, + "nth": 8, + "type": "AccountID" } ], [ - "ParentHash", + "IssuerEncryptedAmount", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 39, + "type": "Blob" + } + ], + [ + "ShareMPTID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 2, - "type": "Hash256" + "type": "Hash192" } ], [ - "TransactionHash", + "CoverRateLiquidation", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "Hash256" + "nth": 63, + "type": "UInt32" } ], [ - "AccountHash", + "EmailHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "Hash256" + "nth": 1, + "type": "Hash128" } ], [ - "PreviousTxnID", + "ConfidentialOutstandingAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "Hash256" + "nth": 32, + "type": "UInt64" } ], [ - "LedgerIndex", + "CoverRateMinimum", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "Hash256" + "nth": 62, + "type": "UInt32" } ], [ - "WalletLocator", + "LoanBrokerNode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 7, - "type": "Hash256" + "nth": 31, + "type": "UInt64" } ], [ - "RootIndex", + "LoanSequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 8, - "type": "Hash256" + "nth": 61, + "type": "UInt32" } ], [ - "AccountTxnID", + "VaultNode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 9, - "type": "Hash256" + "nth": 30, + "type": "UInt64" } ], [ - "NFTokenID", + "PaymentTotal", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 10, - "type": "Hash256" + "nth": 60, + "type": "UInt32" } ], [ - "EmitParentTxnID", + "LockedAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 11, - "type": "Hash256" + "nth": 29, + "type": "UInt64" } ], [ - "EmitNonce", + "NextPaymentDueDate", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 12, - "type": "Hash256" + "nth": 58, + "type": "UInt32" } ], [ - "EmitHookHash", + "IssuerNode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 13, - "type": "Hash256" + "nth": 27, + "type": "UInt64" } ], [ - "AMMID", + "WithdrawalPolicy", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 14, - "type": "Hash256" + "nth": 20, + "type": "UInt8" } ], [ - "BookDirectory", + "PreviousPaymentDueDate", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "Hash256" + "nth": 57, + "type": "UInt32" } ], [ - "InvoiceID", + "MPTAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 17, - "type": "Hash256" + "nth": 26, + "type": "UInt64" } ], [ - "Nickname", + "WasLockingChainSend", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 18, - "type": "Hash256" + "nth": 19, + "type": "UInt8" } ], [ - "Amendment", + "GracePeriod", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, - "type": "Hash256" + "nth": 56, + "type": "UInt32" } ], [ - "Digest", + "OutstandingAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 21, - "type": "Hash256" + "nth": 25, + "type": "UInt64" } ], [ - "Channel", + "PaymentInterval", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, - "type": "Hash256" + "nth": 55, + "type": "UInt32" } ], [ - "ConsensusHash", + "HookResult", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 23, - "type": "Hash256" + "nth": 18, + "type": "UInt8" } ], [ - "CheckID", + "MaximumAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 24, - "type": "Hash256" + "type": "UInt64" } ], [ - "ValidatedHash", + "UNLModifyDisabling", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 25, - "type": "Hash256" + "nth": 17, + "type": "UInt8" } ], [ - "PreviousPageMin", + "StartDate", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 26, - "type": "Hash256" + "nth": 54, + "type": "UInt32" } ], [ - "NextPageMin", + "AssetPrice", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 27, - "type": "Hash256" + "nth": 23, + "type": "UInt64" } ], [ - "NFTokenBuyOffer", + "TickSize", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 28, - "type": "Hash256" + "nth": 16, + "type": "UInt8" } ], [ - "NFTokenSellOffer", + "MutableFlags", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 29, - "type": "Hash256" + "nth": 53, + "type": "UInt32" } ], [ - "HookStateKey", + "XChainAccountClaimCount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 30, - "type": "Hash256" + "nth": 22, + "type": "UInt64" } ], [ - "HookHash", + "ManagementFeeOutstanding", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 31, - "type": "Hash256" + "nth": 17, + "type": "Number" } ], [ - "HookNamespace", + "PermissionValue", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 32, - "type": "Hash256" + "nth": 52, + "type": "UInt32" } ], [ - "HookSetTxnID", + "XChainAccountCreateCount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 33, - "type": "Hash256" + "nth": 21, + "type": "UInt64" } ], [ - "DomainID", + "PeriodicPayment", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 34, - "type": "Hash256" + "nth": 16, + "type": "Number" } ], [ - "VaultID", + "OracleDocumentID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 35, - "type": "Hash256" + "nth": 51, + "type": "UInt32" } ], [ - "ParentBatchID", + "XChainClaimID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 36, - "type": "Hash256" + "nth": 20, + "type": "UInt64" } ], [ - "LoanBrokerID", + "TotalValueOutstanding", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 37, - "type": "Hash256" + "nth": 15, + "type": "Number" } ], [ - "LoanID", + "FirstNFTokenSequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 38, - "type": "Hash256" + "nth": 50, + "type": "UInt32" } ], [ - "hash", + "ReferenceCount", { - "isSerialized": false, - "isSigningField": false, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "nth": 257, - "type": "Hash256" + "nth": 19, + "type": "UInt64" } ], [ - "index", + "Validation", { "isSerialized": false, "isSigningField": false, "isVLEncoded": false, - "nth": 258, - "type": "Hash256" + "nth": 257, + "type": "Validation" } ], [ - "Amount", + "PrincipalRequested", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "Amount" + "nth": 14, + "type": "Number" } ], [ - "Balance", + "HookReturnCode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "Amount" + "nth": 18, + "type": "UInt64" } ], [ - "LimitAmount", + "PrincipalOutstanding", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "Amount" + "nth": 13, + "type": "Number" } ], [ - "TakerPays", + "VoteWeight", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "Amount" + "nth": 48, + "type": "UInt32" } ], [ - "TakerGets", + "HookInstructionCount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "Amount" + "nth": 17, + "type": "UInt64" } ], [ - "LowLimit", + "ClosePaymentFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "Amount" + "nth": 12, + "type": "Number" } ], [ - "HighLimit", + "EmitCallback", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 7, - "type": "Amount" + "isVLEncoded": true, + "nth": 10, + "type": "AccountID" } ], [ - "Fee", + "DestinationEncryptedAmount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 8, - "type": "Amount" + "isVLEncoded": true, + "nth": 41, + "type": "Blob" } ], [ - "SendMax", + "TakerGetsMPT", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 9, - "type": "Amount" + "nth": 4, + "type": "Hash192" } ], [ - "DeliverMin", + "HookOn", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 10, - "type": "Amount" + "nth": 16, + "type": "UInt64" } ], [ - "Amount2", + "LoanOriginationFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 11, - "type": "Amount" + "nth": 9, + "type": "Number" } ], [ - "BidMin", + "HolderEncryptedAmount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, - "type": "Amount" + "isVLEncoded": true, + "nth": 38, + "type": "Blob" } ], [ - "BidMax", + "MPTokenIssuanceID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 13, - "type": "Amount" + "nth": 1, + "type": "Hash192" } ], [ - "MinimumOffer", + "BurnedNFTokens", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "Amount" + "nth": 44, + "type": "UInt32" } ], [ - "RippleEscrow", + "EmitBurden", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 17, - "type": "Amount" + "nth": 13, + "type": "UInt64" } ], [ - "DeliveredAmount", + "CoverAvailable", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 18, - "type": "Amount" + "nth": 8, + "type": "Number" } ], [ - "NFTokenBrokerFee", + "MintedNFTokens", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, - "type": "Amount" + "nth": 43, + "type": "UInt32" } ], [ - "BaseFeeDrops", + "NFTokenOfferNode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, - "type": "Amount" + "nth": 12, + "type": "UInt64" } ], [ - "ReserveBaseDrops", + "DebtMaximum", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 23, - "type": "Amount" + "nth": 7, + "type": "Number" } ], [ - "ReserveIncrementDrops", + "AssetScale", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 24, - "type": "Amount" + "nth": 5, + "type": "UInt8" } ], [ - "LPTokenOut", + "NFTokenTaxon", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 25, - "type": "Amount" + "nth": 42, + "type": "UInt32" } ], [ - "LPTokenIn", + "ServerVersion", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 26, - "type": "Amount" + "nth": 11, + "type": "UInt64" } ], [ - "EPrice", + "DebtTotal", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 27, - "type": "Amount" + "nth": 6, + "type": "Number" } ], [ - "Price", + "TicketSequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 28, - "type": "Amount" + "nth": 41, + "type": "UInt32" } ], [ - "SignatureReward", + "Scale", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 29, - "type": "Amount" + "nth": 4, + "type": "UInt8" } ], [ - "MinAccountCreateAmount", + "Cookie", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 30, - "type": "Amount" + "nth": 10, + "type": "UInt64" } ], [ - "LPTokenBalance", + "LossUnrealized", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 31, - "type": "Amount" + "nth": 5, + "type": "Number" } ], [ - "PublicKey", + "TransactionResult", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 1, - "type": "Blob" + "isVLEncoded": false, + "nth": 3, + "type": "UInt8" } ], [ - "MessageKey", + "TicketCount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 2, - "type": "Blob" + "isVLEncoded": false, + "nth": 40, + "type": "UInt32" } ], [ - "SigningPubKey", + "DestinationNode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 3, - "type": "Blob" + "isVLEncoded": false, + "nth": 9, + "type": "UInt64" } ], [ - "TxnSignature", + "AssetsTotal", { "isSerialized": true, - "isSigningField": false, - "isVLEncoded": true, + "isSigningField": true, + "isVLEncoded": false, "nth": 4, - "type": "Blob" + "type": "Number" } ], [ - "URI", + "Method", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 5, - "type": "Blob" + "isVLEncoded": false, + "nth": 2, + "type": "UInt8" } ], [ - "Signature", + "SettleDelay", { "isSerialized": true, - "isSigningField": false, - "isVLEncoded": true, - "nth": 6, - "type": "Blob" + "isSigningField": true, + "isVLEncoded": false, + "nth": 39, + "type": "UInt32" } ], [ - "Domain", + "HighNode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 7, - "type": "Blob" + "isVLEncoded": false, + "nth": 8, + "type": "UInt64" } ], [ - "FundCode", + "ParentBatchID", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 8, - "type": "Blob" + "isVLEncoded": false, + "nth": 36, + "type": "Hash256" } ], [ - "RemoveCode", + "TakerGets", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 9, - "type": "Blob" + "isVLEncoded": false, + "nth": 5, + "type": "Amount" } ], [ - "ExpireCode", + "AssetsMaximum", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 10, - "type": "Blob" + "isVLEncoded": false, + "nth": 3, + "type": "Number" } ], [ - "CreateCode", + "CloseResolution", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 11, - "type": "Blob" + "isVLEncoded": false, + "nth": 1, + "type": "UInt8" } ], [ - "MemoType", + "SignerListID", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 12, - "type": "Blob" + "isVLEncoded": false, + "nth": 38, + "type": "UInt32" } ], [ - "MemoData", + "LowNode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 13, - "type": "Blob" + "isVLEncoded": false, + "nth": 7, + "type": "UInt64" } ], [ - "MemoFormat", + "VaultID", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 14, - "type": "Blob" + "isVLEncoded": false, + "nth": 35, + "type": "Hash256" } ], [ - "Fulfillment", + "TakerPays", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 16, - "type": "Blob" + "isVLEncoded": false, + "nth": 4, + "type": "Amount" } ], [ - "Condition", + "AssetsAvailable", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 17, - "type": "Blob" + "isVLEncoded": false, + "nth": 2, + "type": "Number" } ], [ - "MasterSignature", + "BatchSigners", { "isSerialized": true, "isSigningField": false, - "isVLEncoded": true, - "nth": 18, - "type": "Blob" + "isVLEncoded": false, + "nth": 31, + "type": "STArray" } ], [ - "UNLModifyValidator", + "FinishAfter", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 19, - "type": "Blob" + "isVLEncoded": false, + "nth": 37, + "type": "UInt32" } ], [ - "ValidatorToDisable", + "ExchangeRate", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 20, - "type": "Blob" + "isVLEncoded": false, + "nth": 6, + "type": "UInt64" } ], [ - "ValidatorToReEnable", + "DomainID", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 21, - "type": "Blob" + "isVLEncoded": false, + "nth": 34, + "type": "Hash256" } ], [ - "HookStateData", + "LimitAmount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 22, - "type": "Blob" + "isVLEncoded": false, + "nth": 3, + "type": "Amount" } ], [ - "HookReturnString", + "Number", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 23, - "type": "Blob" + "isVLEncoded": false, + "nth": 1, + "type": "Number" } ], [ - "HookParameterName", + "RawTransactions", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 24, - "type": "Blob" + "isVLEncoded": false, + "nth": 30, + "type": "STArray" } ], [ - "HookParameterValue", + "CancelAfter", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 25, - "type": "Blob" + "isVLEncoded": false, + "nth": 36, + "type": "UInt32" } ], [ - "DIDDocument", + "BaseFee", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 26, - "type": "Blob" + "isVLEncoded": false, + "nth": 5, + "type": "UInt64" } ], [ - "Data", + "HookSetTxnID", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 27, - "type": "Blob" + "isVLEncoded": false, + "nth": 33, + "type": "Hash256" } ], [ - "AssetClass", + "Balance", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 28, - "type": "Blob" + "isVLEncoded": false, + "nth": 2, + "type": "Amount" } ], [ - "Provider", + "Permissions", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, + "isVLEncoded": false, "nth": 29, - "type": "Blob" + "type": "STArray" } ], [ - "MPTokenMetadata", + "SignerQuorum", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 30, - "type": "Blob" + "isVLEncoded": false, + "nth": 35, + "type": "UInt32" } ], [ - "CredentialType", + "OwnerNode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 31, - "type": "Blob" + "isVLEncoded": false, + "nth": 4, + "type": "UInt64" } ], [ - "Account", + "HookNamespace", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 1, - "type": "AccountID" + "isVLEncoded": false, + "nth": 32, + "type": "Hash256" } ], [ - "Owner", + "Amount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 2, - "type": "AccountID" + "isVLEncoded": false, + "nth": 1, + "type": "Amount" } ], [ - "Destination", + "AcceptedCredentials", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 3, - "type": "AccountID" + "isVLEncoded": false, + "nth": 28, + "type": "STArray" } ], [ - "Issuer", + "ClearFlag", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 4, - "type": "AccountID" + "isVLEncoded": false, + "nth": 34, + "type": "UInt32" } ], [ - "Authorize", + "BookNode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 5, - "type": "AccountID" + "isVLEncoded": false, + "nth": 3, + "type": "UInt64" } ], [ - "Unauthorize", + "UnauthorizeCredentials", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 6, - "type": "AccountID" + "isVLEncoded": false, + "nth": 27, + "type": "STArray" } ], [ - "RegularKey", + "SetFlag", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 8, - "type": "AccountID" + "isVLEncoded": false, + "nth": 33, + "type": "UInt32" } ], [ - "NFTokenMinter", + "IndexPrevious", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 9, - "type": "AccountID" + "isVLEncoded": false, + "nth": 2, + "type": "UInt64" } ], [ - "EmitCallback", + "AuthorizeCredentials", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 10, - "type": "AccountID" + "isVLEncoded": false, + "nth": 26, + "type": "STArray" } ], [ - "Holder", + "ReserveIncrement", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 11, - "type": "AccountID" + "isVLEncoded": false, + "nth": 32, + "type": "UInt32" } ], [ - "Delegate", + "IndexNext", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 12, - "type": "AccountID" + "isVLEncoded": false, + "nth": 1, + "type": "UInt64" } ], [ - "HookAccount", + "SignerWeight", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 16, - "type": "AccountID" + "isVLEncoded": false, + "nth": 3, + "type": "UInt16" } ], [ - "OtherChainSource", + "OverpaymentInterestRate", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 18, - "type": "AccountID" + "isVLEncoded": false, + "nth": 68, + "type": "UInt32" } ], [ - "OtherChainDestination", + "TransactionType", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 19, - "type": "AccountID" + "isVLEncoded": false, + "nth": 2, + "type": "UInt16" } ], [ - "AttestationSignerAccount", + "CloseInterestRate", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 20, - "type": "AccountID" + "isVLEncoded": false, + "nth": 67, + "type": "UInt32" } ], [ - "AttestationRewardAccount", + "InterestRate", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 21, - "type": "AccountID" + "isVLEncoded": false, + "nth": 65, + "type": "UInt32" } ], [ - "LockingChainDoor", + "OverpaymentFee", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 22, - "type": "AccountID" + "isVLEncoded": false, + "nth": 64, + "type": "UInt32" } ], [ - "IssuingChainDoor", + "index", { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": true, - "nth": 23, - "type": "AccountID" + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 258, + "type": "Hash256" } ], [ - "Subject", + "hash", { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": true, - "nth": 24, - "type": "AccountID" + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "Hash256" } ], [ - "Borrower", + "SubjectNode", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 25, - "type": "AccountID" + "isVLEncoded": false, + "nth": 28, + "type": "UInt64" } ], [ - "Counterparty", + "PaymentRemaining", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 26, - "type": "AccountID" + "isVLEncoded": false, + "nth": 59, + "type": "UInt32" } ], [ - "Number", + "EmitGeneration", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "Number" + "nth": 46, + "type": "UInt32" } ], [ - "AssetsAvailable", + "HookStateCount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "Number" + "nth": 45, + "type": "UInt32" } ], [ - "AssetsMaximum", + "HookHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "Number" + "nth": 31, + "type": "Hash256" } ], [ - "AssetsTotal", + "HookStateKey", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "Number" + "nth": 30, + "type": "Hash256" } ], [ - "LossUnrealized", + "NFTokenSellOffer", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "Number" + "nth": 29, + "type": "Hash256" } ], [ - "DebtTotal", + "AuthAccounts", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "Number" + "nth": 25, + "type": "STArray" } ], [ - "DebtMaximum", + "ReserveBase", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 7, - "type": "Number" + "nth": 31, + "type": "UInt32" } ], [ - "CoverAvailable", + "NextPageMin", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 8, - "type": "Number" + "nth": 27, + "type": "Hash256" } ], [ - "LoanOriginationFee", + "OperationLimit", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 9, - "type": "Number" + "nth": 29, + "type": "UInt32" } ], [ - "LoanServiceFee", + "PreviousPageMin", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 10, - "type": "Number" + "nth": 26, + "type": "Hash256" } ], [ - "LatePaymentFee", + "MemoFormat", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 11, - "type": "Number" + "isVLEncoded": true, + "nth": 14, + "type": "Blob" } ], [ - "ClosePaymentFee", + "Transaction", { - "isSerialized": true, - "isSigningField": true, + "isSerialized": false, + "isSigningField": false, "isVLEncoded": false, - "nth": 12, - "type": "Number" + "nth": 257, + "type": "Transaction" } ], [ - "PrincipalOutstanding", + "XChainCreateAccountAttestations", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 13, - "type": "Number" + "nth": 22, + "type": "STArray" } ], [ - "PrincipalRequested", + "TransactionIndex", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 14, - "type": "Number" + "nth": 28, + "type": "UInt32" } ], [ - "TotalValueOutstanding", + "ValidatedHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 15, - "type": "Number" + "nth": 25, + "type": "Hash256" } ], [ - "PeriodicPayment", + "XChainClaimAttestations", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "Number" + "nth": 21, + "type": "STArray" } ], [ - "ManagementFeeOutstanding", + "LastLedgerSequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 17, - "type": "Number" + "nth": 27, + "type": "UInt32" } ], [ - "LoanScale", + "CheckID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "Int32" + "nth": 24, + "type": "Hash256" } ], [ - "TransactionMetaData", + "HookGrants", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "STObject" + "nth": 20, + "type": "STArray" } ], [ - "CreatedNode", + "FirstLedgerSequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "STObject" + "nth": 26, + "type": "UInt32" } ], [ - "DeletedNode", + "ConsensusHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "STObject" + "nth": 23, + "type": "Hash256" } ], [ - "ModifiedNode", + "HookParameters", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "STObject" + "nth": 19, + "type": "STArray" } ], [ - "PreviousFields", + "OfferSequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "STObject" + "nth": 25, + "type": "UInt32" } ], [ - "FinalFields", + "Channel", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 7, - "type": "STObject" + "nth": 22, + "type": "Hash256" } ], [ - "NewFields", + "HookExecutions", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 8, - "type": "STObject" + "nth": 18, + "type": "STArray" } ], [ - "TemplateEntry", + "LoadFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 9, - "type": "STObject" + "nth": 24, + "type": "UInt32" } ], [ - "Memo", + "Digest", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 10, - "type": "STObject" + "nth": 21, + "type": "Hash256" } ], [ - "SignerEntry", + "DisabledValidators", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 11, - "type": "STObject" + "nth": 17, + "type": "STArray" } ], [ - "NFToken", + "BondAmount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 12, - "type": "STObject" + "nth": 23, + "type": "UInt32" } ], [ - "EmitDetails", + "Majorities", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 13, - "type": "STObject" + "nth": 16, + "type": "STArray" } ], [ - "Hook", + "StampEscrow", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 14, - "type": "STObject" + "nth": 22, + "type": "UInt32" } ], [ - "Permission", + "Amendment", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 15, - "type": "STObject" + "nth": 19, + "type": "Hash256" } ], [ - "Signer", + "QualityOut", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "STObject" + "nth": 21, + "type": "UInt32" } ], [ - "Majority", + "Nickname", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 18, - "type": "STObject" + "type": "Hash256" } ], [ - "DisabledValidator", + "QualityIn", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, - "type": "STObject" + "nth": 20, + "type": "UInt32" } ], [ - "EmittedTxn", + "InvoiceID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, - "type": "STObject" + "nth": 17, + "type": "Hash256" } ], [ - "HookExecution", + "AdditionalBooks", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 21, - "type": "STObject" + "nth": 13, + "type": "STArray" } ], [ - "HookDefinition", + "LowQualityOut", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, - "type": "STObject" + "nth": 19, + "type": "UInt32" } ], [ - "HookParameter", + "BookDirectory", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 23, - "type": "STObject" + "nth": 16, + "type": "Hash256" } ], [ - "HookGrant", + "VoteSlots", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 24, - "type": "STObject" + "nth": 12, + "type": "STArray" } ], [ - "VoteEntry", + "LowQualityIn", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 25, - "type": "STObject" + "nth": 18, + "type": "UInt32" } ], [ - "AuctionSlot", + "Hooks", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 26, - "type": "STObject" + "nth": 11, + "type": "STArray" } ], [ - "AuthAccount", + "HighQualityOut", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 27, - "type": "STObject" + "nth": 17, + "type": "UInt32" } ], [ - "XChainClaimProofSig", + "AMMID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 28, - "type": "STObject" + "nth": 14, + "type": "Hash256" } ], [ - "XChainCreateAccountProofSig", + "NFTokens", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 29, - "type": "STObject" + "nth": 10, + "type": "STArray" } ], [ - "XChainClaimAttestationCollectionElement", + "HighQualityIn", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 30, - "type": "STObject" + "nth": 16, + "type": "UInt32" } ], [ - "XChainCreateAccountAttestationCollectionElement", + "EmitHookHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 31, - "type": "STObject" + "nth": 13, + "type": "Hash256" } ], [ - "PriceData", + "Memos", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 32, - "type": "STObject" + "nth": 9, + "type": "STArray" } ], [ - "Credential", + "LastUpdateTime", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 33, - "type": "STObject" + "nth": 15, + "type": "UInt32" } ], [ - "RawTransaction", + "EmitNonce", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 34, - "type": "STObject" + "nth": 12, + "type": "Hash256" } ], [ - "BatchSigner", + "TakerGetsIssuer", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 35, - "type": "STObject" + "nth": 4, + "type": "Hash160" } ], [ - "Book", + "AffectedNodes", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 36, - "type": "STObject" + "nth": 8, + "type": "STArray" } ], [ - "CounterpartySignature", + "DestinationTag", { "isSerialized": true, - "isSigningField": false, + "isSigningField": true, "isVLEncoded": false, - "nth": 37, - "type": "STObject" + "nth": 14, + "type": "UInt32" } ], [ - "Signers", + "EmitParentTxnID", { "isSerialized": true, - "isSigningField": false, + "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "STArray" + "nth": 11, + "type": "Hash256" } ], [ - "SignerEntries", + "TakerGetsCurrency", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "STArray" + "nth": 3, + "type": "Hash160" } ], [ - "Template", + "Sufficient", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, + "nth": 7, "type": "STArray" } ], [ - "Necessary", + "OwnerCount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 6, - "type": "STArray" + "nth": 13, + "type": "UInt32" } ], [ - "Sufficient", + "RootIndex", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 7, - "type": "STArray" + "nth": 8, + "type": "Hash256" } ], [ - "AffectedNodes", + "SignerEntries", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 8, + "nth": 4, "type": "STArray" } ], [ - "Memos", + "BatchSigner", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 9, - "type": "STArray" + "nth": 35, + "type": "STObject" } ], [ - "NFTokens", + "Expiration", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 10, - "type": "STArray" + "type": "UInt32" } ], [ - "Hooks", + "Paths", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 11, - "type": "STArray" + "nth": 1, + "type": "PathSet" } ], [ - "VoteSlots", + "WalletLocator", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 12, - "type": "STArray" + "nth": 7, + "type": "Hash256" } ], [ - "AdditionalBooks", + "Signers", { "isSerialized": true, - "isSigningField": true, + "isSigningField": false, "isVLEncoded": false, - "nth": 13, + "nth": 3, "type": "STArray" } ], [ - "Majorities", + "RawTransaction", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "STArray" + "nth": 34, + "type": "STObject" } ], [ - "DisabledValidators", + "SigningTime", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 17, - "type": "STArray" + "nth": 9, + "type": "UInt32" } ], [ - "HookExecutions", + "LedgerIndex", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 18, - "type": "STArray" + "nth": 6, + "type": "Hash256" } ], [ - "HookParameters", + "Credential", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, - "type": "STArray" + "nth": 33, + "type": "STObject" } ], [ - "HookGrants", + "ParentCloseTime", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, - "type": "STArray" + "nth": 8, + "type": "UInt32" } ], [ - "XChainClaimAttestations", + "PreviousTxnID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 21, - "type": "STArray" + "nth": 5, + "type": "Hash256" } ], [ - "XChainCreateAccountAttestations", + "PriceData", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, - "type": "STArray" + "nth": 32, + "type": "STObject" } ], [ - "PriceDataSeries", + "CloseTime", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 24, - "type": "STArray" + "nth": 7, + "type": "UInt32" } ], [ - "AuthAccounts", + "AccountHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 25, - "type": "STArray" + "nth": 4, + "type": "Hash256" } ], [ - "AuthorizeCredentials", + "XChainCreateAccountAttestationCollectionElement", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 26, - "type": "STArray" + "nth": 31, + "type": "STObject" } ], [ - "UnauthorizeCredentials", + "LedgerSequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 27, - "type": "STArray" + "nth": 6, + "type": "UInt32" } ], [ - "AcceptedCredentials", + "TransactionHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 28, - "type": "STArray" + "nth": 3, + "type": "Hash256" } ], [ - "Permissions", + "XChainClaimAttestationCollectionElement", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 29, - "type": "STArray" + "nth": 30, + "type": "STObject" } ], [ - "RawTransactions", + "PreviousTxnLgrSeq", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 30, - "type": "STArray" + "nth": 5, + "type": "UInt32" } ], [ - "BatchSigners", + "ParentHash", { "isSerialized": true, - "isSigningField": false, + "isSigningField": true, "isVLEncoded": false, - "nth": 31, - "type": "STArray" + "nth": 2, + "type": "Hash256" } ], [ - "CloseResolution", + "XChainCreateAccountProofSig", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "UInt8" + "nth": 29, + "type": "STObject" } ], [ - "Method", + "Sequence", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "UInt8" + "nth": 4, + "type": "UInt32" } ], [ - "TransactionResult", + "Signer", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "UInt8" + "nth": 16, + "type": "STObject" } ], [ - "Scale", + "ManagementFeeRate", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "UInt8" + "nth": 22, + "type": "UInt16" } ], [ - "AssetScale", + "LedgerHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 5, - "type": "UInt8" + "nth": 1, + "type": "Hash256" } ], [ - "TickSize", + "XChainClaimProofSig", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 16, - "type": "UInt8" + "nth": 28, + "type": "STObject" } ], [ - "UNLModifyDisabling", + "SourceTag", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 17, - "type": "UInt8" + "nth": 3, + "type": "UInt32" } ], [ - "HookResult", + "Permission", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 18, - "type": "UInt8" + "nth": 15, + "type": "STObject" } ], [ - "WasLockingChainSend", + "LedgerFixType", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, - "type": "UInt8" + "nth": 21, + "type": "UInt16" } ], [ - "WithdrawalPolicy", + "AuthAccount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, - "type": "UInt8" + "nth": 27, + "type": "STObject" } ], [ - "TakerPaysCurrency", + "Flags", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "Hash160" + "nth": 2, + "type": "UInt32" } ], [ - "TakerPaysIssuer", + "Hook", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "Hash160" + "nth": 14, + "type": "STObject" } ], [ - "TakerGetsCurrency", + "HookApiVersion", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "Hash160" + "nth": 20, + "type": "UInt16" } ], [ - "TakerGetsIssuer", + "NFToken", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "Hash160" + "nth": 12, + "type": "STObject" } ], [ - "Paths", + "HookEmitCount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "PathSet" + "nth": 18, + "type": "UInt16" } ], [ - "Indexes", + "SignerEntry", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 1, - "type": "Vector256" + "isVLEncoded": false, + "nth": 11, + "type": "STObject" } ], [ - "Hashes", + "HookStateChangeCount", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 2, - "type": "Vector256" + "isVLEncoded": false, + "nth": 17, + "type": "UInt16" } ], [ - "Amendments", + "Memo", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 3, - "type": "Vector256" + "isVLEncoded": false, + "nth": 10, + "type": "STObject" } ], [ - "NFTokenOffers", + "Version", { "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 4, - "type": "Vector256" + "isVLEncoded": false, + "nth": 16, + "type": "UInt16" } ], [ - "CredentialIDs", + "Indexes", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 5, + "nth": 1, "type": "Vector256" } ], [ - "MPTokenIssuanceID", + "HighLimit", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "Hash192" + "nth": 7, + "type": "Amount" } ], [ - "ShareMPTID", + "LoanID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "Hash192" + "nth": 38, + "type": "Hash256" } ], [ - "LockingChainIssue", + "TakerPaysCurrency", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, "nth": 1, - "type": "Issue" + "type": "Hash160" } ], [ - "IssuingChainIssue", + "ConfidentialBalanceVersion", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 2, - "type": "Issue" + "nth": 69, + "type": "UInt32" } ], [ - "Asset", + "TransferFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 3, - "type": "Issue" + "nth": 4, + "type": "UInt16" } ], [ - "Asset2", + "LowLimit", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 4, - "type": "Issue" + "nth": 6, + "type": "Amount" } ], [ - "XChainBridge", + "LoanBrokerID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "XChainBridge" + "nth": 37, + "type": "Hash256" } ], [ - "BaseAsset", + "DiscountedFee", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 1, - "type": "Currency" + "nth": 6, + "type": "UInt16" } ], [ - "QuoteAsset", + "LateInterestRate", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, + "nth": 66, + "type": "UInt32" + } + ], + [ + "LedgerEntryType", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "UInt16" + } + ], + [ + "Hashes", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, "nth": 2, - "type": "Currency" + "type": "Vector256" } ], [ - "Transaction", + "Fee", { - "isSerialized": false, - "isSigningField": false, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "nth": 257, - "type": "Transaction" + "nth": 8, + "type": "Amount" } ], [ - "LedgerEntry", + "BlindingFactor", { - "isSerialized": false, - "isSigningField": false, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "nth": 257, - "type": "LedgerEntry" + "nth": 39, + "type": "Hash256" } ], [ - "Validation", + "TradingFee", { - "isSerialized": false, - "isSigningField": false, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "nth": 257, - "type": "Validation" + "nth": 5, + "type": "UInt16" } ], [ - "Metadata", + "AccountTxnID", { - "isSerialized": false, - "isSigningField": false, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "nth": 257, - "type": "Metadata" + "nth": 9, + "type": "Hash256" } - ] - ], - "LEDGER_ENTRY_TYPES": { + ], + [ + "Template", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 5, + "type": "STArray" + } + ], + [ + "Book", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 36, + "type": "STObject" + } + ], + [ + "TransferRate", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 11, + "type": "UInt32" + } + ], + [ + "Generic", + { + "isSerialized": false, + "isSigningField": true, + "isVLEncoded": false, + "nth": 0, + "type": "Unknown" + } + ], + [ + "NFTokenID", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 10, + "type": "Hash256" + } + ], + [ + "TakerPaysIssuer", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Hash160" + } + ], + [ + "Necessary", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 6, + "type": "STArray" + } + ], + [ + "CounterpartySignature", + { + "isSerialized": true, + "isSigningField": false, + "isVLEncoded": false, + "nth": 37, + "type": "STObject" + } + ], + [ + "WalletSize", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 12, + "type": "UInt32" + } + ], + [ + "NFTokenBuyOffer", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 28, + "type": "Hash256" + } + ], + [ + "Metadata", + { + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "Metadata" + } + ], + [ + "PriceDataSeries", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 24, + "type": "STArray" + } + ], + [ + "ReferenceFeeUnits", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 30, + "type": "UInt32" + } + ], + [ + "AmountCommitment", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 45, + "type": "Blob" + } + ], + [ + "LedgerEntry", + { + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "LedgerEntry" + } + ], + [ + "AuctionSlot", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 26, + "type": "STObject" + } + ], + [ + "NetworkID", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "UInt32" + } + ], + [ + "EmitDetails", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 13, + "type": "STObject" + } + ], + [ + "HookExecutionIndex", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 19, + "type": "UInt16" + } + ] + ], + "LEDGER_ENTRY_FLAGS": { + "AccountRoot": { + "lsfAllowTrustLineClawback": 2147483648, + "lsfAllowTrustLineLocking": 1073741824, + "lsfDefaultRipple": 8388608, + "lsfDepositAuth": 16777216, + "lsfDisableMaster": 1048576, + "lsfDisallowIncomingCheck": 134217728, + "lsfDisallowIncomingNFTokenOffer": 67108864, + "lsfDisallowIncomingPayChan": 268435456, + "lsfDisallowIncomingTrustline": 536870912, + "lsfDisallowXRP": 524288, + "lsfGlobalFreeze": 4194304, + "lsfNoFreeze": 2097152, + "lsfPasswordSpent": 65536, + "lsfRequireAuth": 262144, + "lsfRequireDestTag": 131072 + }, + "Credential": { + "lsfAccepted": 65536 + }, + "DirNode": { + "lsfNFTokenBuyOffers": 1, + "lsfNFTokenSellOffers": 2 + }, + "Loan": { + "lsfLoanDefault": 65536, + "lsfLoanImpaired": 131072, + "lsfLoanOverpayment": 262144 + }, + "MPToken": { + "lsfMPTAMM": 4, + "lsfMPTAuthorized": 2, + "lsfMPTLocked": 1 + }, + "MPTokenIssuance": { + "lsfMPTCanClawback": 64, + "lsfMPTCanConfidentialAmount": 128, + "lsfMPTCanEscrow": 8, + "lsfMPTCanLock": 2, + "lsfMPTCanTrade": 16, + "lsfMPTCanTransfer": 32, + "lsfMPTLocked": 1, + "lsfMPTRequireAuth": 4 + }, + "MPTokenIssuanceMutable": { + "lsmfMPTCanMutateCanClawback": 64, + "lsmfMPTCanMutateCanEscrow": 8, + "lsmfMPTCanMutateCanLock": 2, + "lsmfMPTCanMutateCanTrade": 16, + "lsmfMPTCanMutateCanTransfer": 32, + "lsmfMPTCanMutateMetadata": 65536, + "lsmfMPTCanMutateRequireAuth": 4, + "lsmfMPTCanMutateTransferFee": 131072, + "lsmfMPTCannotMutateCanConfidentialAmount": 262144 + }, + "NFTokenOffer": { + "lsfSellNFToken": 1 + }, + "Offer": { + "lsfHybrid": 262144, + "lsfPassive": 65536, + "lsfSell": 131072 + }, + "RippleState": { + "lsfAMMNode": 16777216, + "lsfHighAuth": 524288, + "lsfHighDeepFreeze": 67108864, + "lsfHighFreeze": 8388608, + "lsfHighNoRipple": 2097152, + "lsfHighReserve": 131072, + "lsfLowAuth": 262144, + "lsfLowDeepFreeze": 33554432, + "lsfLowFreeze": 4194304, + "lsfLowNoRipple": 1048576, + "lsfLowReserve": 65536 + }, + "SignerList": { + "lsfOneOwnerCount": 65536 + }, + "Vault": { + "lsfVaultPrivate": 65536 + } + }, + "LEDGER_ENTRY_FORMATS": { + "AMM": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "TradingFee", + "optionality": 2 + }, + { + "name": "VoteSlots", + "optionality": 1 + }, + { + "name": "AuctionSlot", + "optionality": 1 + }, + { + "name": "LPTokenBalance", + "optionality": 0 + }, + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "Asset2", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 1 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 1 + } + ], + "AccountRoot": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "Balance", + "optionality": 0 + }, + { + "name": "OwnerCount", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "AccountTxnID", + "optionality": 1 + }, + { + "name": "RegularKey", + "optionality": 1 + }, + { + "name": "EmailHash", + "optionality": 1 + }, + { + "name": "WalletLocator", + "optionality": 1 + }, + { + "name": "WalletSize", + "optionality": 1 + }, + { + "name": "MessageKey", + "optionality": 1 + }, + { + "name": "TransferRate", + "optionality": 1 + }, + { + "name": "Domain", + "optionality": 1 + }, + { + "name": "TickSize", + "optionality": 1 + }, + { + "name": "TicketCount", + "optionality": 1 + }, + { + "name": "NFTokenMinter", + "optionality": 1 + }, + { + "name": "MintedNFTokens", + "optionality": 2 + }, + { + "name": "BurnedNFTokens", + "optionality": 2 + }, + { + "name": "FirstNFTokenSequence", + "optionality": 1 + }, + { + "name": "AMMID", + "optionality": 1 + }, + { + "name": "VaultID", + "optionality": 1 + }, + { + "name": "LoanBrokerID", + "optionality": 1 + } + ], + "Amendments": [ + { + "name": "Amendments", + "optionality": 1 + }, + { + "name": "Majorities", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 1 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 1 + } + ], + "Bridge": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "SignatureReward", + "optionality": 0 + }, + { + "name": "MinAccountCreateAmount", + "optionality": 1 + }, + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "XChainClaimID", + "optionality": 0 + }, + { + "name": "XChainAccountCreateCount", + "optionality": 0 + }, + { + "name": "XChainAccountClaimCount", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "Check": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "SendMax", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "DestinationNode", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "InvoiceID", + "optionality": 1 + }, + { + "name": "SourceTag", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "Credential": [ + { + "name": "Subject", + "optionality": 0 + }, + { + "name": "Issuer", + "optionality": 0 + }, + { + "name": "CredentialType", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "URI", + "optionality": 1 + }, + { + "name": "IssuerNode", + "optionality": 0 + }, + { + "name": "SubjectNode", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "DID": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "DIDDocument", + "optionality": 1 + }, + { + "name": "URI", + "optionality": 1 + }, + { + "name": "Data", + "optionality": 1 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "Delegate": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Authorize", + "optionality": 0 + }, + { + "name": "Permissions", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "DepositPreauth": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Authorize", + "optionality": 1 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "AuthorizeCredentials", + "optionality": 1 + } + ], + "DirectoryNode": [ + { + "name": "Owner", + "optionality": 1 + }, + { + "name": "TakerPaysCurrency", + "optionality": 1 + }, + { + "name": "TakerPaysIssuer", + "optionality": 1 + }, + { + "name": "TakerPaysMPT", + "optionality": 1 + }, + { + "name": "TakerGetsCurrency", + "optionality": 1 + }, + { + "name": "TakerGetsIssuer", + "optionality": 1 + }, + { + "name": "TakerGetsMPT", + "optionality": 1 + }, + { + "name": "ExchangeRate", + "optionality": 1 + }, + { + "name": "Indexes", + "optionality": 0 + }, + { + "name": "RootIndex", + "optionality": 0 + }, + { + "name": "IndexNext", + "optionality": 1 + }, + { + "name": "IndexPrevious", + "optionality": 1 + }, + { + "name": "NFTokenID", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 1 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + } + ], + "Escrow": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 1 + }, + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Condition", + "optionality": 1 + }, + { + "name": "CancelAfter", + "optionality": 1 + }, + { + "name": "FinishAfter", + "optionality": 1 + }, + { + "name": "SourceTag", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "DestinationNode", + "optionality": 1 + }, + { + "name": "TransferRate", + "optionality": 1 + }, + { + "name": "IssuerNode", + "optionality": 1 + } + ], + "FeeSettings": [ + { + "name": "BaseFee", + "optionality": 1 + }, + { + "name": "ReferenceFeeUnits", + "optionality": 1 + }, + { + "name": "ReserveBase", + "optionality": 1 + }, + { + "name": "ReserveIncrement", + "optionality": 1 + }, + { + "name": "BaseFeeDrops", + "optionality": 1 + }, + { + "name": "ReserveBaseDrops", + "optionality": 1 + }, + { + "name": "ReserveIncrementDrops", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 1 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 1 + } + ], + "LedgerHashes": [ + { + "name": "FirstLedgerSequence", + "optionality": 1 + }, + { + "name": "LastLedgerSequence", + "optionality": 1 + }, + { + "name": "Hashes", + "optionality": 0 + } + ], + "Loan": [ + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "LoanBrokerNode", + "optionality": 0 + }, + { + "name": "LoanBrokerID", + "optionality": 0 + }, + { + "name": "LoanSequence", + "optionality": 0 + }, + { + "name": "Borrower", + "optionality": 0 + }, + { + "name": "LoanOriginationFee", + "optionality": 2 + }, + { + "name": "LoanServiceFee", + "optionality": 2 + }, + { + "name": "LatePaymentFee", + "optionality": 2 + }, + { + "name": "ClosePaymentFee", + "optionality": 2 + }, + { + "name": "OverpaymentFee", + "optionality": 2 + }, + { + "name": "InterestRate", + "optionality": 2 + }, + { + "name": "LateInterestRate", + "optionality": 2 + }, + { + "name": "CloseInterestRate", + "optionality": 2 + }, + { + "name": "OverpaymentInterestRate", + "optionality": 2 + }, + { + "name": "StartDate", + "optionality": 0 + }, + { + "name": "PaymentInterval", + "optionality": 0 + }, + { + "name": "GracePeriod", + "optionality": 2 + }, + { + "name": "PreviousPaymentDueDate", + "optionality": 2 + }, + { + "name": "NextPaymentDueDate", + "optionality": 2 + }, + { + "name": "PaymentRemaining", + "optionality": 2 + }, + { + "name": "PeriodicPayment", + "optionality": 0 + }, + { + "name": "PrincipalOutstanding", + "optionality": 2 + }, + { + "name": "TotalValueOutstanding", + "optionality": 2 + }, + { + "name": "ManagementFeeOutstanding", + "optionality": 2 + }, + { + "name": "LoanScale", + "optionality": 2 + } + ], + "LoanBroker": [ + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "VaultNode", + "optionality": 0 + }, + { + "name": "VaultID", + "optionality": 0 + }, + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Owner", + "optionality": 0 + }, + { + "name": "LoanSequence", + "optionality": 0 + }, + { + "name": "Data", + "optionality": 2 + }, + { + "name": "ManagementFeeRate", + "optionality": 2 + }, + { + "name": "OwnerCount", + "optionality": 2 + }, + { + "name": "DebtTotal", + "optionality": 2 + }, + { + "name": "DebtMaximum", + "optionality": 2 + }, + { + "name": "CoverAvailable", + "optionality": 2 + }, + { + "name": "CoverRateMinimum", + "optionality": 2 + }, + { + "name": "CoverRateLiquidation", + "optionality": 2 + } + ], + "MPToken": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "MPTokenIssuanceID", + "optionality": 0 + }, + { + "name": "MPTAmount", + "optionality": 2 + }, + { + "name": "LockedAmount", + "optionality": 1 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "ConfidentialBalanceInbox", + "optionality": 1 + }, + { + "name": "ConfidentialBalanceSpending", + "optionality": 1 + }, + { + "name": "ConfidentialBalanceVersion", + "optionality": 2 + }, + { + "name": "IssuerEncryptedBalance", + "optionality": 1 + }, + { + "name": "AuditorEncryptedBalance", + "optionality": 1 + }, + { + "name": "HolderEncryptionKey", + "optionality": 1 + } + ], + "MPTokenIssuance": [ + { + "name": "Issuer", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "TransferFee", + "optionality": 2 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "AssetScale", + "optionality": 2 + }, + { + "name": "MaximumAmount", + "optionality": 1 + }, + { + "name": "OutstandingAmount", + "optionality": 0 + }, + { + "name": "LockedAmount", + "optionality": 1 + }, + { + "name": "MPTokenMetadata", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "DomainID", + "optionality": 1 + }, + { + "name": "MutableFlags", + "optionality": 2 + }, + { + "name": "IssuerEncryptionKey", + "optionality": 1 + }, + { + "name": "AuditorEncryptionKey", + "optionality": 1 + }, + { + "name": "ConfidentialOutstandingAmount", + "optionality": 2 + } + ], + "NFTokenOffer": [ + { + "name": "Owner", + "optionality": 0 + }, + { + "name": "NFTokenID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "NFTokenOfferNode", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 1 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "NFTokenPage": [ + { + "name": "PreviousPageMin", + "optionality": 1 + }, + { + "name": "NextPageMin", + "optionality": 1 + }, + { + "name": "NFTokens", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "NegativeUNL": [ + { + "name": "DisabledValidators", + "optionality": 1 + }, + { + "name": "ValidatorToDisable", + "optionality": 1 + }, + { + "name": "ValidatorToReEnable", + "optionality": 1 + }, + { + "name": "PreviousTxnID", + "optionality": 1 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 1 + } + ], + "Offer": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "TakerPays", + "optionality": 0 + }, + { + "name": "TakerGets", + "optionality": 0 + }, + { + "name": "BookDirectory", + "optionality": 0 + }, + { + "name": "BookNode", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + }, + { + "name": "AdditionalBooks", + "optionality": 1 + } + ], + "Oracle": [ + { + "name": "Owner", + "optionality": 0 + }, + { + "name": "OracleDocumentID", + "optionality": 1 + }, + { + "name": "Provider", + "optionality": 0 + }, + { + "name": "PriceDataSeries", + "optionality": 0 + }, + { + "name": "AssetClass", + "optionality": 0 + }, + { + "name": "LastUpdateTime", + "optionality": 0 + }, + { + "name": "URI", + "optionality": 1 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "PayChannel": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 1 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Balance", + "optionality": 0 + }, + { + "name": "PublicKey", + "optionality": 0 + }, + { + "name": "SettleDelay", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "CancelAfter", + "optionality": 1 + }, + { + "name": "SourceTag", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "DestinationNode", + "optionality": 1 + } + ], + "PermissionedDomain": [ + { + "name": "Owner", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "AcceptedCredentials", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "RippleState": [ + { + "name": "Balance", + "optionality": 0 + }, + { + "name": "LowLimit", + "optionality": 0 + }, + { + "name": "HighLimit", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "LowNode", + "optionality": 1 + }, + { + "name": "LowQualityIn", + "optionality": 1 + }, + { + "name": "LowQualityOut", + "optionality": 1 + }, + { + "name": "HighNode", + "optionality": 1 + }, + { + "name": "HighQualityIn", + "optionality": 1 + }, + { + "name": "HighQualityOut", + "optionality": 1 + } + ], + "SignerList": [ + { + "name": "Owner", + "optionality": 1 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "SignerQuorum", + "optionality": 0 + }, + { + "name": "SignerEntries", + "optionality": 0 + }, + { + "name": "SignerListID", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "Ticket": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "TicketSequence", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "Vault": [ + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "Owner", + "optionality": 0 + }, + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Data", + "optionality": 1 + }, + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "AssetsTotal", + "optionality": 2 + }, + { + "name": "AssetsAvailable", + "optionality": 2 + }, + { + "name": "AssetsMaximum", + "optionality": 2 + }, + { + "name": "LossUnrealized", + "optionality": 2 + }, + { + "name": "ShareMPTID", + "optionality": 0 + }, + { + "name": "WithdrawalPolicy", + "optionality": 0 + }, + { + "name": "Scale", + "optionality": 2 + } + ], + "XChainOwnedClaimID": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "XChainClaimID", + "optionality": 0 + }, + { + "name": "OtherChainSource", + "optionality": 0 + }, + { + "name": "XChainClaimAttestations", + "optionality": 0 + }, + { + "name": "SignatureReward", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "XChainOwnedCreateAccountClaimID": [ + { + "name": "Account", + "optionality": 0 + }, + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "XChainAccountCreateCount", + "optionality": 0 + }, + { + "name": "XChainCreateAccountAttestations", + "optionality": 0 + }, + { + "name": "OwnerNode", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 0 + }, + { + "name": "PreviousTxnLgrSeq", + "optionality": 0 + } + ], + "common": [ + { + "name": "LedgerIndex", + "optionality": 1 + }, + { + "name": "LedgerEntryType", + "optionality": 0 + }, + { + "name": "Flags", + "optionality": 0 + } + ] + }, + "LEDGER_ENTRY_TYPES": { "AMM": 121, "AccountRoot": 97, "Amendments": 102, @@ -3464,6 +5128,1631 @@ "XChainOwnedClaimID": 113, "XChainOwnedCreateAccountClaimID": 116 }, + "TRANSACTION_FLAGS": { + "AMMClawback": { + "tfClawTwoAssets": 1 + }, + "AMMDeposit": { + "tfLPToken": 65536, + "tfLimitLPToken": 4194304, + "tfOneAssetLPToken": 2097152, + "tfSingleAsset": 524288, + "tfTwoAsset": 1048576, + "tfTwoAssetIfEmpty": 8388608 + }, + "AMMWithdraw": { + "tfLPToken": 65536, + "tfLimitLPToken": 4194304, + "tfOneAssetLPToken": 2097152, + "tfOneAssetWithdrawAll": 262144, + "tfSingleAsset": 524288, + "tfTwoAsset": 1048576, + "tfWithdrawAll": 131072 + }, + "AccountSet": { + "tfAllowXRP": 2097152, + "tfDisallowXRP": 1048576, + "tfOptionalAuth": 524288, + "tfOptionalDestTag": 131072, + "tfRequireAuth": 262144, + "tfRequireDestTag": 65536 + }, + "Batch": { + "tfAllOrNothing": 65536, + "tfIndependent": 524288, + "tfOnlyOne": 131072, + "tfUntilFailure": 262144 + }, + "EnableAmendment": { + "tfGotMajority": 65536, + "tfLostMajority": 131072 + }, + "LoanManage": { + "tfLoanDefault": 65536, + "tfLoanImpair": 131072, + "tfLoanUnimpair": 262144 + }, + "LoanPay": { + "tfLoanFullPayment": 131072, + "tfLoanLatePayment": 262144, + "tfLoanOverpayment": 65536 + }, + "LoanSet": { + "tfLoanOverpayment": 65536 + }, + "MPTokenAuthorize": { + "tfMPTUnauthorize": 1 + }, + "MPTokenIssuanceCreate": { + "tfMPTCanClawback": 64, + "tfMPTCanConfidentialAmount": 128, + "tfMPTCanEscrow": 8, + "tfMPTCanLock": 2, + "tfMPTCanTrade": 16, + "tfMPTCanTransfer": 32, + "tfMPTRequireAuth": 4 + }, + "MPTokenIssuanceSet": { + "tfMPTLock": 1, + "tfMPTUnlock": 2 + }, + "NFTokenCreateOffer": { + "tfSellNFToken": 1 + }, + "NFTokenMint": { + "tfBurnable": 1, + "tfMutable": 16, + "tfOnlyXRP": 2, + "tfTransferable": 8 + }, + "OfferCreate": { + "tfFillOrKill": 262144, + "tfHybrid": 1048576, + "tfImmediateOrCancel": 131072, + "tfPassive": 65536, + "tfSell": 524288 + }, + "Payment": { + "tfLimitQuality": 262144, + "tfNoRippleDirect": 65536, + "tfPartialPayment": 131072 + }, + "PaymentChannelClaim": { + "tfClose": 131072, + "tfRenew": 65536 + }, + "TrustSet": { + "tfClearDeepFreeze": 8388608, + "tfClearFreeze": 2097152, + "tfClearNoRipple": 262144, + "tfSetDeepFreeze": 4194304, + "tfSetFreeze": 1048576, + "tfSetNoRipple": 131072, + "tfSetfAuth": 65536 + }, + "VaultCreate": { + "tfVaultPrivate": 65536, + "tfVaultShareNonTransferable": 131072 + }, + "XChainModifyBridge": { + "tfClearAccountCreateAmount": 65536 + }, + "universal": { + "tfFullyCanonicalSig": 2147483648, + "tfInnerBatchTxn": 1073741824 + } + }, + "TRANSACTION_FORMATS": { + "AMMBid": [ + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "Asset2", + "optionality": 0 + }, + { + "name": "BidMin", + "optionality": 1 + }, + { + "name": "BidMax", + "optionality": 1 + }, + { + "name": "AuthAccounts", + "optionality": 1 + } + ], + "AMMClawback": [ + { + "name": "Holder", + "optionality": 0 + }, + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "Asset2", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 1 + } + ], + "AMMCreate": [ + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Amount2", + "optionality": 0 + }, + { + "name": "TradingFee", + "optionality": 0 + } + ], + "AMMDelete": [ + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "Asset2", + "optionality": 0 + } + ], + "AMMDeposit": [ + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "Asset2", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 1 + }, + { + "name": "Amount2", + "optionality": 1 + }, + { + "name": "EPrice", + "optionality": 1 + }, + { + "name": "LPTokenOut", + "optionality": 1 + }, + { + "name": "TradingFee", + "optionality": 1 + } + ], + "AMMVote": [ + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "Asset2", + "optionality": 0 + }, + { + "name": "TradingFee", + "optionality": 0 + } + ], + "AMMWithdraw": [ + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "Asset2", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 1 + }, + { + "name": "Amount2", + "optionality": 1 + }, + { + "name": "EPrice", + "optionality": 1 + }, + { + "name": "LPTokenIn", + "optionality": 1 + } + ], + "AccountDelete": [ + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "CredentialIDs", + "optionality": 1 + } + ], + "AccountSet": [ + { + "name": "EmailHash", + "optionality": 1 + }, + { + "name": "WalletLocator", + "optionality": 1 + }, + { + "name": "WalletSize", + "optionality": 1 + }, + { + "name": "MessageKey", + "optionality": 1 + }, + { + "name": "Domain", + "optionality": 1 + }, + { + "name": "TransferRate", + "optionality": 1 + }, + { + "name": "SetFlag", + "optionality": 1 + }, + { + "name": "ClearFlag", + "optionality": 1 + }, + { + "name": "TickSize", + "optionality": 1 + }, + { + "name": "NFTokenMinter", + "optionality": 1 + } + ], + "Batch": [ + { + "name": "RawTransactions", + "optionality": 0 + }, + { + "name": "BatchSigners", + "optionality": 1 + } + ], + "CheckCancel": [ + { + "name": "CheckID", + "optionality": 0 + } + ], + "CheckCash": [ + { + "name": "CheckID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 1 + }, + { + "name": "DeliverMin", + "optionality": 1 + } + ], + "CheckCreate": [ + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "SendMax", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "InvoiceID", + "optionality": 1 + } + ], + "Clawback": [ + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Holder", + "optionality": 1 + } + ], + "ConfidentialMPTClawback": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + }, + { + "name": "Holder", + "optionality": 0 + }, + { + "name": "MPTAmount", + "optionality": 0 + }, + { + "name": "ZKProof", + "optionality": 0 + } + ], + "ConfidentialMPTConvert": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + }, + { + "name": "MPTAmount", + "optionality": 0 + }, + { + "name": "HolderEncryptionKey", + "optionality": 1 + }, + { + "name": "HolderEncryptedAmount", + "optionality": 0 + }, + { + "name": "IssuerEncryptedAmount", + "optionality": 0 + }, + { + "name": "AuditorEncryptedAmount", + "optionality": 1 + }, + { + "name": "BlindingFactor", + "optionality": 0 + }, + { + "name": "ZKProof", + "optionality": 1 + } + ], + "ConfidentialMPTConvertBack": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + }, + { + "name": "MPTAmount", + "optionality": 0 + }, + { + "name": "HolderEncryptedAmount", + "optionality": 0 + }, + { + "name": "IssuerEncryptedAmount", + "optionality": 0 + }, + { + "name": "AuditorEncryptedAmount", + "optionality": 1 + }, + { + "name": "BlindingFactor", + "optionality": 0 + }, + { + "name": "ZKProof", + "optionality": 0 + }, + { + "name": "BalanceCommitment", + "optionality": 0 + } + ], + "ConfidentialMPTMergeInbox": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + } + ], + "ConfidentialMPTSend": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "SenderEncryptedAmount", + "optionality": 0 + }, + { + "name": "DestinationEncryptedAmount", + "optionality": 0 + }, + { + "name": "IssuerEncryptedAmount", + "optionality": 0 + }, + { + "name": "AuditorEncryptedAmount", + "optionality": 1 + }, + { + "name": "ZKProof", + "optionality": 0 + }, + { + "name": "AmountCommitment", + "optionality": 0 + }, + { + "name": "BalanceCommitment", + "optionality": 0 + }, + { + "name": "CredentialIDs", + "optionality": 1 + } + ], + "CredentialAccept": [ + { + "name": "Issuer", + "optionality": 0 + }, + { + "name": "CredentialType", + "optionality": 0 + } + ], + "CredentialCreate": [ + { + "name": "Subject", + "optionality": 0 + }, + { + "name": "CredentialType", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "URI", + "optionality": 1 + } + ], + "CredentialDelete": [ + { + "name": "Subject", + "optionality": 1 + }, + { + "name": "Issuer", + "optionality": 1 + }, + { + "name": "CredentialType", + "optionality": 0 + } + ], + "DIDDelete": [], + "DIDSet": [ + { + "name": "DIDDocument", + "optionality": 1 + }, + { + "name": "URI", + "optionality": 1 + }, + { + "name": "Data", + "optionality": 1 + } + ], + "DelegateSet": [ + { + "name": "Authorize", + "optionality": 0 + }, + { + "name": "Permissions", + "optionality": 0 + } + ], + "DepositPreauth": [ + { + "name": "Authorize", + "optionality": 1 + }, + { + "name": "Unauthorize", + "optionality": 1 + }, + { + "name": "AuthorizeCredentials", + "optionality": 1 + }, + { + "name": "UnauthorizeCredentials", + "optionality": 1 + } + ], + "EnableAmendment": [ + { + "name": "LedgerSequence", + "optionality": 0 + }, + { + "name": "Amendment", + "optionality": 0 + } + ], + "EscrowCancel": [ + { + "name": "Owner", + "optionality": 0 + }, + { + "name": "OfferSequence", + "optionality": 0 + } + ], + "EscrowCreate": [ + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Condition", + "optionality": 1 + }, + { + "name": "CancelAfter", + "optionality": 1 + }, + { + "name": "FinishAfter", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + } + ], + "EscrowFinish": [ + { + "name": "Owner", + "optionality": 0 + }, + { + "name": "OfferSequence", + "optionality": 0 + }, + { + "name": "Fulfillment", + "optionality": 1 + }, + { + "name": "Condition", + "optionality": 1 + }, + { + "name": "CredentialIDs", + "optionality": 1 + } + ], + "LedgerStateFix": [ + { + "name": "LedgerFixType", + "optionality": 0 + }, + { + "name": "Owner", + "optionality": 1 + } + ], + "LoanBrokerCoverClawback": [ + { + "name": "LoanBrokerID", + "optionality": 1 + }, + { + "name": "Amount", + "optionality": 1 + } + ], + "LoanBrokerCoverDeposit": [ + { + "name": "LoanBrokerID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + } + ], + "LoanBrokerCoverWithdraw": [ + { + "name": "LoanBrokerID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + } + ], + "LoanBrokerDelete": [ + { + "name": "LoanBrokerID", + "optionality": 0 + } + ], + "LoanBrokerSet": [ + { + "name": "VaultID", + "optionality": 0 + }, + { + "name": "LoanBrokerID", + "optionality": 1 + }, + { + "name": "Data", + "optionality": 1 + }, + { + "name": "ManagementFeeRate", + "optionality": 1 + }, + { + "name": "DebtMaximum", + "optionality": 1 + }, + { + "name": "CoverRateMinimum", + "optionality": 1 + }, + { + "name": "CoverRateLiquidation", + "optionality": 1 + } + ], + "LoanDelete": [ + { + "name": "LoanID", + "optionality": 0 + } + ], + "LoanManage": [ + { + "name": "LoanID", + "optionality": 0 + } + ], + "LoanPay": [ + { + "name": "LoanID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + } + ], + "LoanSet": [ + { + "name": "LoanBrokerID", + "optionality": 0 + }, + { + "name": "Data", + "optionality": 1 + }, + { + "name": "Counterparty", + "optionality": 1 + }, + { + "name": "CounterpartySignature", + "optionality": 1 + }, + { + "name": "LoanOriginationFee", + "optionality": 1 + }, + { + "name": "LoanServiceFee", + "optionality": 1 + }, + { + "name": "LatePaymentFee", + "optionality": 1 + }, + { + "name": "ClosePaymentFee", + "optionality": 1 + }, + { + "name": "OverpaymentFee", + "optionality": 1 + }, + { + "name": "InterestRate", + "optionality": 1 + }, + { + "name": "LateInterestRate", + "optionality": 1 + }, + { + "name": "CloseInterestRate", + "optionality": 1 + }, + { + "name": "OverpaymentInterestRate", + "optionality": 1 + }, + { + "name": "PrincipalRequested", + "optionality": 0 + }, + { + "name": "PaymentTotal", + "optionality": 1 + }, + { + "name": "PaymentInterval", + "optionality": 1 + }, + { + "name": "GracePeriod", + "optionality": 1 + } + ], + "MPTokenAuthorize": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + }, + { + "name": "Holder", + "optionality": 1 + } + ], + "MPTokenIssuanceCreate": [ + { + "name": "AssetScale", + "optionality": 1 + }, + { + "name": "TransferFee", + "optionality": 1 + }, + { + "name": "MaximumAmount", + "optionality": 1 + }, + { + "name": "MPTokenMetadata", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + }, + { + "name": "MutableFlags", + "optionality": 1 + } + ], + "MPTokenIssuanceDestroy": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + } + ], + "MPTokenIssuanceSet": [ + { + "name": "MPTokenIssuanceID", + "optionality": 0 + }, + { + "name": "Holder", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + }, + { + "name": "MPTokenMetadata", + "optionality": 1 + }, + { + "name": "TransferFee", + "optionality": 1 + }, + { + "name": "MutableFlags", + "optionality": 1 + }, + { + "name": "IssuerEncryptionKey", + "optionality": 1 + }, + { + "name": "AuditorEncryptionKey", + "optionality": 1 + } + ], + "NFTokenAcceptOffer": [ + { + "name": "NFTokenBuyOffer", + "optionality": 1 + }, + { + "name": "NFTokenSellOffer", + "optionality": 1 + }, + { + "name": "NFTokenBrokerFee", + "optionality": 1 + } + ], + "NFTokenBurn": [ + { + "name": "NFTokenID", + "optionality": 0 + }, + { + "name": "Owner", + "optionality": 1 + } + ], + "NFTokenCancelOffer": [ + { + "name": "NFTokenOffers", + "optionality": 0 + } + ], + "NFTokenCreateOffer": [ + { + "name": "NFTokenID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 1 + }, + { + "name": "Owner", + "optionality": 1 + }, + { + "name": "Expiration", + "optionality": 1 + } + ], + "NFTokenMint": [ + { + "name": "NFTokenTaxon", + "optionality": 0 + }, + { + "name": "TransferFee", + "optionality": 1 + }, + { + "name": "Issuer", + "optionality": 1 + }, + { + "name": "URI", + "optionality": 1 + }, + { + "name": "Amount", + "optionality": 1 + }, + { + "name": "Destination", + "optionality": 1 + }, + { + "name": "Expiration", + "optionality": 1 + } + ], + "NFTokenModify": [ + { + "name": "NFTokenID", + "optionality": 0 + }, + { + "name": "Owner", + "optionality": 1 + }, + { + "name": "URI", + "optionality": 1 + } + ], + "OfferCancel": [ + { + "name": "OfferSequence", + "optionality": 0 + } + ], + "OfferCreate": [ + { + "name": "TakerPays", + "optionality": 0 + }, + { + "name": "TakerGets", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + }, + { + "name": "OfferSequence", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + } + ], + "OracleDelete": [ + { + "name": "OracleDocumentID", + "optionality": 0 + } + ], + "OracleSet": [ + { + "name": "OracleDocumentID", + "optionality": 0 + }, + { + "name": "Provider", + "optionality": 1 + }, + { + "name": "URI", + "optionality": 1 + }, + { + "name": "AssetClass", + "optionality": 1 + }, + { + "name": "LastUpdateTime", + "optionality": 0 + }, + { + "name": "PriceDataSeries", + "optionality": 0 + } + ], + "Payment": [ + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "SendMax", + "optionality": 1 + }, + { + "name": "Paths", + "optionality": 2 + }, + { + "name": "InvoiceID", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "DeliverMin", + "optionality": 1 + }, + { + "name": "CredentialIDs", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + } + ], + "PaymentChannelClaim": [ + { + "name": "Channel", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 1 + }, + { + "name": "Balance", + "optionality": 1 + }, + { + "name": "Signature", + "optionality": 1 + }, + { + "name": "PublicKey", + "optionality": 1 + }, + { + "name": "CredentialIDs", + "optionality": 1 + } + ], + "PaymentChannelCreate": [ + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "SettleDelay", + "optionality": 0 + }, + { + "name": "PublicKey", + "optionality": 0 + }, + { + "name": "CancelAfter", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + } + ], + "PaymentChannelFund": [ + { + "name": "Channel", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Expiration", + "optionality": 1 + } + ], + "PermissionedDomainDelete": [ + { + "name": "DomainID", + "optionality": 0 + } + ], + "PermissionedDomainSet": [ + { + "name": "DomainID", + "optionality": 1 + }, + { + "name": "AcceptedCredentials", + "optionality": 0 + } + ], + "SetFee": [ + { + "name": "LedgerSequence", + "optionality": 1 + }, + { + "name": "BaseFee", + "optionality": 1 + }, + { + "name": "ReferenceFeeUnits", + "optionality": 1 + }, + { + "name": "ReserveBase", + "optionality": 1 + }, + { + "name": "ReserveIncrement", + "optionality": 1 + }, + { + "name": "BaseFeeDrops", + "optionality": 1 + }, + { + "name": "ReserveBaseDrops", + "optionality": 1 + }, + { + "name": "ReserveIncrementDrops", + "optionality": 1 + } + ], + "SetRegularKey": [ + { + "name": "RegularKey", + "optionality": 1 + } + ], + "SignerListSet": [ + { + "name": "SignerQuorum", + "optionality": 0 + }, + { + "name": "SignerEntries", + "optionality": 1 + } + ], + "TicketCreate": [ + { + "name": "TicketCount", + "optionality": 0 + } + ], + "TrustSet": [ + { + "name": "LimitAmount", + "optionality": 1 + }, + { + "name": "QualityIn", + "optionality": 1 + }, + { + "name": "QualityOut", + "optionality": 1 + } + ], + "UNLModify": [ + { + "name": "UNLModifyDisabling", + "optionality": 0 + }, + { + "name": "LedgerSequence", + "optionality": 0 + }, + { + "name": "UNLModifyValidator", + "optionality": 0 + } + ], + "VaultClawback": [ + { + "name": "VaultID", + "optionality": 0 + }, + { + "name": "Holder", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 1 + } + ], + "VaultCreate": [ + { + "name": "Asset", + "optionality": 0 + }, + { + "name": "AssetsMaximum", + "optionality": 1 + }, + { + "name": "MPTokenMetadata", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + }, + { + "name": "WithdrawalPolicy", + "optionality": 1 + }, + { + "name": "Data", + "optionality": 1 + }, + { + "name": "Scale", + "optionality": 1 + } + ], + "VaultDelete": [ + { + "name": "VaultID", + "optionality": 0 + } + ], + "VaultDeposit": [ + { + "name": "VaultID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + } + ], + "VaultSet": [ + { + "name": "VaultID", + "optionality": 0 + }, + { + "name": "AssetsMaximum", + "optionality": 1 + }, + { + "name": "DomainID", + "optionality": 1 + }, + { + "name": "Data", + "optionality": 1 + } + ], + "VaultWithdraw": [ + { + "name": "VaultID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 1 + }, + { + "name": "DestinationTag", + "optionality": 1 + } + ], + "XChainAccountCreateCommit": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "SignatureReward", + "optionality": 0 + } + ], + "XChainAddAccountCreateAttestation": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "AttestationSignerAccount", + "optionality": 0 + }, + { + "name": "PublicKey", + "optionality": 0 + }, + { + "name": "Signature", + "optionality": 0 + }, + { + "name": "OtherChainSource", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "AttestationRewardAccount", + "optionality": 0 + }, + { + "name": "WasLockingChainSend", + "optionality": 0 + }, + { + "name": "XChainAccountCreateCount", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "SignatureReward", + "optionality": 0 + } + ], + "XChainAddClaimAttestation": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "AttestationSignerAccount", + "optionality": 0 + }, + { + "name": "PublicKey", + "optionality": 0 + }, + { + "name": "Signature", + "optionality": 0 + }, + { + "name": "OtherChainSource", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "AttestationRewardAccount", + "optionality": 0 + }, + { + "name": "WasLockingChainSend", + "optionality": 0 + }, + { + "name": "XChainClaimID", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 1 + } + ], + "XChainClaim": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "XChainClaimID", + "optionality": 0 + }, + { + "name": "Destination", + "optionality": 0 + }, + { + "name": "DestinationTag", + "optionality": 1 + }, + { + "name": "Amount", + "optionality": 0 + } + ], + "XChainCommit": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "XChainClaimID", + "optionality": 0 + }, + { + "name": "Amount", + "optionality": 0 + }, + { + "name": "OtherChainDestination", + "optionality": 1 + } + ], + "XChainCreateBridge": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "SignatureReward", + "optionality": 0 + }, + { + "name": "MinAccountCreateAmount", + "optionality": 1 + } + ], + "XChainCreateClaimID": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "SignatureReward", + "optionality": 0 + }, + { + "name": "OtherChainSource", + "optionality": 0 + } + ], + "XChainModifyBridge": [ + { + "name": "XChainBridge", + "optionality": 0 + }, + { + "name": "SignatureReward", + "optionality": 1 + }, + { + "name": "MinAccountCreateAmount", + "optionality": 1 + } + ], + "common": [ + { + "name": "TransactionType", + "optionality": 0 + }, + { + "name": "Flags", + "optionality": 1 + }, + { + "name": "SourceTag", + "optionality": 1 + }, + { + "name": "Account", + "optionality": 0 + }, + { + "name": "Sequence", + "optionality": 0 + }, + { + "name": "PreviousTxnID", + "optionality": 1 + }, + { + "name": "LastLedgerSequence", + "optionality": 1 + }, + { + "name": "AccountTxnID", + "optionality": 1 + }, + { + "name": "Fee", + "optionality": 0 + }, + { + "name": "OperationLimit", + "optionality": 1 + }, + { + "name": "Memos", + "optionality": 1 + }, + { + "name": "SigningPubKey", + "optionality": 0 + }, + { + "name": "TicketSequence", + "optionality": 1 + }, + { + "name": "TxnSignature", + "optionality": 1 + }, + { + "name": "Signers", + "optionality": 1 + }, + { + "name": "NetworkID", + "optionality": 1 + }, + { + "name": "Delegate", + "optionality": 1 + } + ] + }, "TRANSACTION_RESULTS": { "tecAMM_ACCOUNT": 168, "tecAMM_BALANCE": 163, @@ -3474,6 +6763,7 @@ "tecARRAY_EMPTY": 190, "tecARRAY_TOO_LARGE": 191, "tecBAD_CREDENTIALS": 193, + "tecBAD_PROOF": 199, "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, "tecCLAIM": 100, "tecCRYPTOCONDITION_ERROR": 146, @@ -3485,7 +6775,6 @@ "tecFAILED_PROCESSING": 105, "tecFROZEN": 137, "tecHAS_OBLIGATIONS": 151, - "tecHOOK_REJECTED": 153, "tecINCOMPLETE": 169, "tecINSUFFICIENT_FUNDS": 159, "tecINSUFFICIENT_PAYMENT": 161, @@ -3505,7 +6794,6 @@ "tecNFTOKEN_OFFER_TYPE_MISMATCH": 157, "tecNO_ALTERNATIVE_KEY": 130, "tecNO_AUTH": 134, - "tecNO_DELEGATE_PERMISSION": 198, "tecNO_DST": 124, "tecNO_DST_INSUF_XRP": 125, "tecNO_ENTRY": 140, @@ -3549,7 +6837,6 @@ "tecXCHAIN_SELF_COMMIT": 184, "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 179, "tecXCHAIN_WRONG_CHAIN": 176, - "tefALREADY": -198, "tefBAD_ADD_AUTH": -197, "tefBAD_AUTH": -196, @@ -3572,7 +6859,6 @@ "tefPAST_SEQ": -190, "tefTOO_BIG": -181, "tefWRONG_PRIOR": -189, - "telBAD_DOMAIN": -398, "telBAD_PATH_COUNT": -397, "telBAD_PUBLIC_KEY": -396, @@ -3590,16 +6876,17 @@ "telNO_DST_PARTIAL": -393, "telREQUIRES_NETWORK_ID": -385, "telWRONG_NETWORK": -386, - "temARRAY_EMPTY": -253, "temARRAY_TOO_LARGE": -252, "temBAD_AMM_TOKENS": -261, "temBAD_AMOUNT": -298, + "temBAD_CIPHERTEXT": -248, "temBAD_CURRENCY": -297, "temBAD_EXPIRATION": -296, "temBAD_FEE": -295, "temBAD_ISSUER": -294, "temBAD_LIMIT": -293, + "temBAD_MPT": -249, "temBAD_NFTOKEN_TRANSFER_FEE": -262, "temBAD_OFFER": -292, "temBAD_PATH": -291, @@ -3641,11 +6928,11 @@ "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260, - "terADDRESS_COLLISION": -86, "terFUNDS_SPENT": -98, "terINSUF_FEE_B": -97, "terLAST": -91, + "terLOCKED": -84, "terNO_ACCOUNT": -96, "terNO_AMM": -87, "terNO_AUTH": -95, @@ -3657,7 +6944,6 @@ "terPRE_TICKET": -88, "terQUEUED": -89, "terRETRY": -99, - "tesSUCCESS": 0 }, "TRANSACTION_TYPES": { @@ -3675,6 +6961,11 @@ "CheckCash": 17, "CheckCreate": 16, "Clawback": 30, + "ConfidentialMPTClawback": 89, + "ConfidentialMPTConvert": 85, + "ConfidentialMPTConvertBack": 87, + "ConfidentialMPTMergeInbox": 86, + "ConfidentialMPTSend": 88, "CredentialAccept": 59, "CredentialCreate": 58, "CredentialDelete": 60, @@ -3748,6 +7039,8 @@ "Hash160": 17, "Hash192": 21, "Hash256": 5, + "Hash384": 22, + "Hash512": 23, "Int32": 10, "Int64": 11, "Issue": 24, @@ -3761,8 +7054,6 @@ "Transaction": 10001, "UInt16": 1, "UInt32": 2, - "UInt384": 22, - "UInt512": 23, "UInt64": 3, "UInt8": 16, "UInt96": 20, diff --git a/src/lib.rs b/src/lib.rs index 78456c5d..e6b7ca25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,15 @@ pub mod wallet; pub extern crate serde_json; +/// Re-export of the [`mpt_crypto`] safe-wrapper crate (XLS-0096 Confidential +/// MPT cryptography). The `ConfidentialMPT*` transaction models accept +/// pre-computed proof and ciphertext bytes, so callers need this crate to +/// generate that material: ElGamal holder keys, encrypted amounts, blinding +/// factors, and the per-transaction-type zero-knowledge proofs. Available only +/// with the `confidential-mpt` feature. +#[cfg(feature = "confidential-mpt")] +pub extern crate mpt_crypto; + #[cfg(feature = "models")] mod _serde; diff --git a/src/models/transactions/confidential_mpt_clawback.rs b/src/models/transactions/confidential_mpt_clawback.rs new file mode 100644 index 00000000..a85518c2 --- /dev/null +++ b/src/models/transactions/confidential_mpt_clawback.rs @@ -0,0 +1,154 @@ +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + transactions::{Memo, Signer, Transaction, TransactionType}, + Model, ValidateCurrencies, +}; +use crate::models::{FlagCollection, NoFlags}; + +use super::{CommonFields, CommonTransactionBuilder}; + +/// A `ConfidentialMPTClawback` transaction is an issuer-only operation +/// that reclaims a holder's confidential balance, decrypting it via the +/// issuer's mirror key and burning the result (XLS-0096 §11). +/// +/// The 64-byte `ZKProof` is a compact sigma proof that the holder's +/// `IssuerEncryptedBalance` ciphertext encrypts the plaintext `MPTAmount` +/// the issuer is reclaiming. The transaction simultaneously decreases both +/// `OutstandingAmount` and `ConfidentialOutstandingAmount` — effectively +/// burning the clawed-back tokens. +#[skip_serializing_none] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] +#[serde(rename_all = "PascalCase")] +pub struct ConfidentialMPTClawback<'a> { + /// `Account` here is the issuer initiating the clawback. + #[serde(flatten)] + pub common_fields: CommonFields<'a, NoFlags>, + + /// The holder being clawed back. + pub holder: Cow<'a, str>, + + #[serde(rename = "MPTokenIssuanceID")] + pub mptoken_issuance_id: Cow<'a, str>, + + /// The plaintext total amount being reclaimed (decrypted by the issuer + /// from the holder's `IssuerEncryptedBalance` mirror). + #[serde(rename = "MPTAmount")] + pub mpt_amount: Cow<'a, str>, + + /// 64-byte compact Clawback sigma proof. + #[serde(rename = "ZKProof")] + pub zk_proof: Cow<'a, str>, +} + +impl<'a> Model for ConfidentialMPTClawback<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} + +impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTClawback<'a> { + fn get_transaction_type(&self) -> &TransactionType { + self.common_fields.get_transaction_type() + } + + fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { + self.common_fields.get_common_fields() + } + + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + self.common_fields.get_mut_common_fields() + } +} + +impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTClawback<'a> { + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + &mut self.common_fields + } + + fn into_self(self) -> Self { + self + } +} + +impl<'a> ConfidentialMPTClawback<'a> { + #[allow(clippy::too_many_arguments)] + pub fn new( + account: Cow<'a, str>, + account_txn_id: Option>, + fee: Option>, + last_ledger_sequence: Option, + memos: Option>, + sequence: Option, + signers: Option>, + source_tag: Option, + ticket_sequence: Option, + holder: Cow<'a, str>, + mptoken_issuance_id: Cow<'a, str>, + mpt_amount: Cow<'a, str>, + zk_proof: Cow<'a, str>, + ) -> Self { + Self { + common_fields: CommonFields::new( + account, + TransactionType::ConfidentialMPTClawback, + account_txn_id, + fee, + Some(FlagCollection::default()), + last_ledger_sequence, + memos, + None, + sequence, + signers, + None, + source_tag, + ticket_sequence, + None, + ), + holder, + mptoken_issuance_id, + mpt_amount, + zk_proof, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize() { + let tx = ConfidentialMPTClawback { + common_fields: CommonFields { + account: "rIssuerAccount11111111111111".into(), + transaction_type: TransactionType::ConfidentialMPTClawback, + ..Default::default() + }, + holder: "rHolderAccount11111111111111".into(), + mptoken_issuance_id: "610F33".repeat(4).into(), + mpt_amount: "1000".into(), + zk_proof: "a1".repeat(64).into(), + }; + + let json = serde_json::to_string(&tx).unwrap(); + assert!(json.contains("\"TransactionType\":\"ConfidentialMPTClawback\"")); + assert!(json.contains("\"Holder\":\"rHolderAccount")); + + let round_tripped: ConfidentialMPTClawback = serde_json::from_str(&json).unwrap(); + assert_eq!(round_tripped, tx); + } +} diff --git a/src/models/transactions/confidential_mpt_convert.rs b/src/models/transactions/confidential_mpt_convert.rs new file mode 100644 index 00000000..ac23cabd --- /dev/null +++ b/src/models/transactions/confidential_mpt_convert.rs @@ -0,0 +1,278 @@ +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + transactions::{Memo, Signer, Transaction, TransactionType}, + Model, ValidateCurrencies, +}; +use crate::models::{FlagCollection, NoFlags}; + +use super::{CommonFields, CommonTransactionBuilder}; + +/// A `ConfidentialMPTConvert` transaction converts a holder's public MPT +/// balance into confidential form (XLS-0096 §7). +/// +/// On first use it also serves as the **opt-in** for confidential MPTs: the +/// holder registers their `HolderEncryptionKey` and provides a 64-byte +/// Schnorr Proof of Knowledge of the corresponding secret key. +/// +/// On subsequent calls (key already registered) `holder_encryption_key` +/// and `zk_proof` MUST both be absent — those fields are gated by §7.3.1 +/// rules 2 and 3. +#[skip_serializing_none] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] +#[serde(rename_all = "PascalCase")] +pub struct ConfidentialMPTConvert<'a> { + #[serde(flatten)] + pub common_fields: CommonFields<'a, NoFlags>, + + /// 24-byte `MPTokenIssuanceID` of the target MPT. + #[serde(rename = "MPTokenIssuanceID")] + pub mptoken_issuance_id: Cow<'a, str>, + + /// Plaintext amount being converted from public to confidential. + /// Encoded as a u64 string per XRPL's large-integer convention. + #[serde(rename = "MPTAmount")] + pub mpt_amount: Cow<'a, str>, + + /// 66-byte ElGamal ciphertext credited to the holder's `CB_IN`. + pub holder_encrypted_amount: Cow<'a, str>, + + /// 66-byte ElGamal ciphertext credited to the issuer's mirror balance. + pub issuer_encrypted_amount: Cow<'a, str>, + + /// 32-byte ElGamal randomness `r`. Revealed plaintext so validators + /// can deterministically verify the ciphertexts encrypt `mpt_amount`. + pub blinding_factor: Cow<'a, str>, + + /// 33-byte compressed holder ElGamal public key. **Required** on first + /// Convert (key registration); **forbidden** thereafter. + pub holder_encryption_key: Option>, + + /// 66-byte ElGamal ciphertext for the auditor mirror. Required iff the + /// issuance has an `AuditorEncryptionKey` registered. + pub auditor_encrypted_amount: Option>, + + /// 64-byte Schnorr Proof of Knowledge of the holder's secret key. + /// **Required** if `holder_encryption_key` is present; **forbidden** + /// otherwise. + #[serde(rename = "ZKProof")] + pub zk_proof: Option>, +} + +impl<'a> Model for ConfidentialMPTConvert<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} + +impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTConvert<'a> { + fn get_transaction_type(&self) -> &TransactionType { + self.common_fields.get_transaction_type() + } + + fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { + self.common_fields.get_common_fields() + } + + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + self.common_fields.get_mut_common_fields() + } +} + +impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTConvert<'a> { + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + &mut self.common_fields + } + + fn into_self(self) -> Self { + self + } +} + +impl<'a> ConfidentialMPTConvert<'a> { + #[allow(clippy::too_many_arguments)] + pub fn new( + account: Cow<'a, str>, + account_txn_id: Option>, + fee: Option>, + last_ledger_sequence: Option, + memos: Option>, + sequence: Option, + signers: Option>, + source_tag: Option, + ticket_sequence: Option, + mptoken_issuance_id: Cow<'a, str>, + mpt_amount: Cow<'a, str>, + holder_encrypted_amount: Cow<'a, str>, + issuer_encrypted_amount: Cow<'a, str>, + blinding_factor: Cow<'a, str>, + holder_encryption_key: Option>, + auditor_encrypted_amount: Option>, + zk_proof: Option>, + ) -> Self { + Self { + common_fields: CommonFields::new( + account, + TransactionType::ConfidentialMPTConvert, + account_txn_id, + fee, + Some(FlagCollection::default()), + last_ledger_sequence, + memos, + None, + sequence, + signers, + None, + source_tag, + ticket_sequence, + None, + ), + mptoken_issuance_id, + mpt_amount, + holder_encrypted_amount, + issuer_encrypted_amount, + blinding_factor, + holder_encryption_key, + auditor_encrypted_amount, + zk_proof, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize_first_convert_with_registration() { + let tx = ConfidentialMPTConvert { + common_fields: CommonFields { + account: "rUserAccount11111111111111111111".into(), + transaction_type: TransactionType::ConfidentialMPTConvert, + ..Default::default() + }, + mptoken_issuance_id: "610F33B8EBF7EC795F822A454FB852156AEFE50BE0CB8326338A81CD74801864" + .into(), + mpt_amount: "1000".into(), + holder_encrypted_amount: "AD3F".repeat(33).into(), + issuer_encrypted_amount: "BC2E".repeat(33).into(), + blinding_factor: "EE".repeat(32).into(), + holder_encryption_key: Some("03".to_string() + &"8d".repeat(32)).map(Into::into), + auditor_encrypted_amount: None, + zk_proof: Some("AB".repeat(64).into()), + }; + + let json = serde_json::to_string(&tx).unwrap(); + assert!(json.contains("\"TransactionType\":\"ConfidentialMPTConvert\"")); + assert!(json.contains("\"HolderEncryptionKey\"")); + assert!(json.contains("\"ZKProof\"")); + + let round_tripped: ConfidentialMPTConvert = serde_json::from_str(&json).unwrap(); + assert_eq!(round_tripped, tx); + } + + #[test] + fn test_serialize_subsequent_convert_no_key() { + let tx = ConfidentialMPTConvert { + common_fields: CommonFields { + account: "rUserAccount11111111111111111111".into(), + transaction_type: TransactionType::ConfidentialMPTConvert, + ..Default::default() + }, + mptoken_issuance_id: "610F33".repeat(4).into(), + mpt_amount: "500".into(), + holder_encrypted_amount: "AD3F".repeat(33).into(), + issuer_encrypted_amount: "BC2E".repeat(33).into(), + blinding_factor: "EE".repeat(32).into(), + holder_encryption_key: None, + auditor_encrypted_amount: None, + zk_proof: None, + }; + + let json = serde_json::to_string(&tx).unwrap(); + // Optional absent fields should not appear via skip_serializing_none. + assert!(!json.contains("\"HolderEncryptionKey\"")); + assert!(!json.contains("\"ZKProof\"")); + } + + #[test] + fn test_new_builder_and_accessors() { + let mut tx = ConfidentialMPTConvert::new( + "rUserAccount11111111111111111111".into(), + None, + None, + None, + None, + None, + None, + None, + None, + "610F33".repeat(4).into(), + "1000".into(), + "AD3F".repeat(33).into(), + "BC2E".repeat(33).into(), + "EE".repeat(32).into(), + None, + None, + None, + ) + .with_fee(XRPAmount::from("20000")) + .with_sequence(7); + + // with_fee/with_sequence route through the builder's + // get_mut_common_fields() + into_self(). + assert_eq!(tx.get_common_fields().sequence, Some(7)); + assert_eq!(tx.get_common_fields().fee, Some(XRPAmount::from("20000"))); + assert_eq!( + tx.get_transaction_type(), + &TransactionType::ConfidentialMPTConvert + ); + // No currency amounts to validate, so Model::get_errors succeeds. + assert!(tx.get_errors().is_ok()); + + // Transaction::get_mut_common_fields (distinct from the builder's + // same-named method) — disambiguate via UFCS. + let common = + >::get_mut_common_fields(&mut tx); + assert_eq!(common.sequence, Some(7)); + } + + #[test] + fn test_serialize_with_auditor_mirror() { + let tx = ConfidentialMPTConvert { + common_fields: CommonFields { + account: "rUserAccount11111111111111111111".into(), + transaction_type: TransactionType::ConfidentialMPTConvert, + ..Default::default() + }, + mptoken_issuance_id: "610F33".repeat(4).into(), + mpt_amount: "750".into(), + holder_encrypted_amount: "AD3F".repeat(33).into(), + issuer_encrypted_amount: "BC2E".repeat(33).into(), + blinding_factor: "EE".repeat(32).into(), + holder_encryption_key: None, + // Issuance with a registered AuditorEncryptionKey requires the mirror. + auditor_encrypted_amount: Some("CD".repeat(66).into()), + zk_proof: None, + }; + + let json = serde_json::to_string(&tx).unwrap(); + assert!(json.contains("\"AuditorEncryptedAmount\"")); + + let round_tripped: ConfidentialMPTConvert = serde_json::from_str(&json).unwrap(); + assert_eq!(round_tripped, tx); + } +} diff --git a/src/models/transactions/confidential_mpt_convert_back.rs b/src/models/transactions/confidential_mpt_convert_back.rs new file mode 100644 index 00000000..49dd539c --- /dev/null +++ b/src/models/transactions/confidential_mpt_convert_back.rs @@ -0,0 +1,177 @@ +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + transactions::{Memo, Signer, Transaction, TransactionType}, + Model, ValidateCurrencies, +}; +use crate::models::{FlagCollection, NoFlags}; + +use super::{CommonFields, CommonTransactionBuilder}; + +/// A `ConfidentialMPTConvertBack` transaction converts confidential MPT +/// value back to public form (XLS-0096 §10). The withdrawal amount is +/// revealed plaintext; the holder proves it doesn't exceed their balance +/// without revealing the balance itself. +/// +/// The 816-byte `ZKProof` field carries: +/// - 128 B compact AND-composed sigma (balance ownership + key linkage) +/// - 688 B single Bulletproof (remainder is non-negative) +#[skip_serializing_none] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] +#[serde(rename_all = "PascalCase")] +pub struct ConfidentialMPTConvertBack<'a> { + #[serde(flatten)] + pub common_fields: CommonFields<'a, NoFlags>, + + #[serde(rename = "MPTokenIssuanceID")] + pub mptoken_issuance_id: Cow<'a, str>, + + /// Plaintext withdrawal amount (revealed publicly). + #[serde(rename = "MPTAmount")] + pub mpt_amount: Cow<'a, str>, + + /// 66-byte ElGamal ciphertext to be subtracted from holder's `CB_S`. + pub holder_encrypted_amount: Cow<'a, str>, + + /// 66-byte ElGamal ciphertext to be subtracted from issuer mirror. + pub issuer_encrypted_amount: Cow<'a, str>, + + /// 32-byte ElGamal randomness `r`. Revealed for deterministic + /// verification of the ciphertexts above. + pub blinding_factor: Cow<'a, str>, + + /// 33-byte Pedersen commitment to the holder's current balance. + pub balance_commitment: Cow<'a, str>, + + /// 816-byte composite proof. + #[serde(rename = "ZKProof")] + pub zk_proof: Cow<'a, str>, + + /// 66-byte ciphertext for the auditor mirror. Required iff the + /// issuance has an `AuditorEncryptionKey` registered. + pub auditor_encrypted_amount: Option>, +} + +impl<'a> Model for ConfidentialMPTConvertBack<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} + +impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTConvertBack<'a> { + fn get_transaction_type(&self) -> &TransactionType { + self.common_fields.get_transaction_type() + } + + fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { + self.common_fields.get_common_fields() + } + + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + self.common_fields.get_mut_common_fields() + } +} + +impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTConvertBack<'a> { + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + &mut self.common_fields + } + + fn into_self(self) -> Self { + self + } +} + +impl<'a> ConfidentialMPTConvertBack<'a> { + #[allow(clippy::too_many_arguments)] + pub fn new( + account: Cow<'a, str>, + account_txn_id: Option>, + fee: Option>, + last_ledger_sequence: Option, + memos: Option>, + sequence: Option, + signers: Option>, + source_tag: Option, + ticket_sequence: Option, + mptoken_issuance_id: Cow<'a, str>, + mpt_amount: Cow<'a, str>, + holder_encrypted_amount: Cow<'a, str>, + issuer_encrypted_amount: Cow<'a, str>, + blinding_factor: Cow<'a, str>, + balance_commitment: Cow<'a, str>, + zk_proof: Cow<'a, str>, + auditor_encrypted_amount: Option>, + ) -> Self { + Self { + common_fields: CommonFields::new( + account, + TransactionType::ConfidentialMPTConvertBack, + account_txn_id, + fee, + Some(FlagCollection::default()), + last_ledger_sequence, + memos, + None, + sequence, + signers, + None, + source_tag, + ticket_sequence, + None, + ), + mptoken_issuance_id, + mpt_amount, + holder_encrypted_amount, + issuer_encrypted_amount, + blinding_factor, + balance_commitment, + zk_proof, + auditor_encrypted_amount, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize() { + let tx = ConfidentialMPTConvertBack { + common_fields: CommonFields { + account: "rUserAccount11111111111111111111".into(), + transaction_type: TransactionType::ConfidentialMPTConvertBack, + ..Default::default() + }, + mptoken_issuance_id: "610F33".repeat(4).into(), + mpt_amount: "500".into(), + holder_encrypted_amount: "AD".repeat(66).into(), + issuer_encrypted_amount: "BC".repeat(66).into(), + blinding_factor: "12".repeat(32).into(), + balance_commitment: "03".repeat(33).into(), + zk_proof: "AB".repeat(816).into(), + auditor_encrypted_amount: None, + }; + + let json = serde_json::to_string(&tx).unwrap(); + assert!(json.contains("\"TransactionType\":\"ConfidentialMPTConvertBack\"")); + assert!(json.contains("\"BalanceCommitment\"")); + + let round_tripped: ConfidentialMPTConvertBack = serde_json::from_str(&json).unwrap(); + assert_eq!(round_tripped, tx); + } +} diff --git a/src/models/transactions/confidential_mpt_merge_inbox.rs b/src/models/transactions/confidential_mpt_merge_inbox.rs new file mode 100644 index 00000000..f7581b43 --- /dev/null +++ b/src/models/transactions/confidential_mpt_merge_inbox.rs @@ -0,0 +1,176 @@ +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + transactions::{Memo, Signer, Transaction, TransactionType}, + Model, ValidateCurrencies, +}; +use crate::models::{FlagCollection, NoFlags}; + +use super::{CommonFields, CommonTransactionBuilder}; + +/// A `ConfidentialMPTMergeInbox` transaction merges a holder's confidential +/// inbox balance (`CB_IN`) into their spending balance (`CB_S`) and resets +/// the inbox to a canonical encrypted-zero (XLS-0096 §9). +/// +/// This transaction is **proof-free** — it carries no ZK proof, no +/// ciphertexts, and no commitments. The ledger performs the homomorphic +/// addition deterministically and bumps `ConfidentialBalanceVersion` to +/// invalidate any in-flight proofs that referenced the prior `CB_S`. +/// +/// XLS-0096 §9 / §A.2. +#[skip_serializing_none] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] +#[serde(rename_all = "PascalCase")] +pub struct ConfidentialMPTMergeInbox<'a> { + /// The base fields for all transaction models. + #[serde(flatten)] + pub common_fields: CommonFields<'a, NoFlags>, + + /// 24-byte `MPTokenIssuanceID` (hex-encoded) identifying the issuance + /// being merged. Same format as XLS-33's `MPTokenIssuanceID`. + #[serde(rename = "MPTokenIssuanceID")] + pub mptoken_issuance_id: Cow<'a, str>, +} + +impl<'a> Model for ConfidentialMPTMergeInbox<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} + +impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTMergeInbox<'a> { + fn get_transaction_type(&self) -> &TransactionType { + self.common_fields.get_transaction_type() + } + + fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { + self.common_fields.get_common_fields() + } + + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + self.common_fields.get_mut_common_fields() + } +} + +impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTMergeInbox<'a> { + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + &mut self.common_fields + } + + fn into_self(self) -> Self { + self + } +} + +impl<'a> ConfidentialMPTMergeInbox<'a> { + #[allow(clippy::too_many_arguments)] + pub fn new( + account: Cow<'a, str>, + account_txn_id: Option>, + fee: Option>, + last_ledger_sequence: Option, + memos: Option>, + sequence: Option, + signers: Option>, + source_tag: Option, + ticket_sequence: Option, + mptoken_issuance_id: Cow<'a, str>, + ) -> Self { + Self { + common_fields: CommonFields::new( + account, + TransactionType::ConfidentialMPTMergeInbox, + account_txn_id, + fee, + Some(FlagCollection::default()), + last_ledger_sequence, + memos, + None, + sequence, + signers, + None, + source_tag, + ticket_sequence, + None, + ), + mptoken_issuance_id, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize() { + let tx = ConfidentialMPTMergeInbox { + common_fields: CommonFields { + account: "rUserAccount111111111111111111111".into(), + transaction_type: TransactionType::ConfidentialMPTMergeInbox, + fee: Some("12".into()), + sequence: Some(42), + signing_pub_key: Some("".into()), + ..Default::default() + }, + mptoken_issuance_id: "610F33B8EBF7EC795F822A454FB852156AEFE50BE0CB8326338A81CD74801864" + .into(), + }; + + let json = serde_json::to_string(&tx).unwrap(); + assert!(json.contains("\"TransactionType\":\"ConfidentialMPTMergeInbox\"")); + assert!(json.contains("\"MPTokenIssuanceID\":\"610F33B8")); + + // Round-trip + let round_tripped: ConfidentialMPTMergeInbox = serde_json::from_str(&json).unwrap(); + assert_eq!(round_tripped, tx); + } + + #[test] + fn test_new_builder_and_accessors() { + let mut tx = ConfidentialMPTMergeInbox::new( + "rUserAccount111111111111111111111".into(), + None, + None, + None, + None, + None, + None, + None, + None, + "610F33".repeat(4).into(), + ) + .with_fee(XRPAmount::from("20000")) + .with_sequence(7); + + // with_fee/with_sequence route through the builder's + // get_mut_common_fields() + into_self(). + assert_eq!(tx.get_common_fields().sequence, Some(7)); + assert_eq!(tx.get_common_fields().fee, Some(XRPAmount::from("20000"))); + assert_eq!( + tx.get_transaction_type(), + &TransactionType::ConfidentialMPTMergeInbox + ); + // No currency amounts to validate, so Model::get_errors succeeds. + assert!(tx.get_errors().is_ok()); + + // Transaction::get_mut_common_fields (distinct from the builder's + // same-named method) — disambiguate via UFCS. + let common = + >::get_mut_common_fields(&mut tx); + assert_eq!(common.sequence, Some(7)); + } +} diff --git a/src/models/transactions/confidential_mpt_send.rs b/src/models/transactions/confidential_mpt_send.rs new file mode 100644 index 00000000..323e76c5 --- /dev/null +++ b/src/models/transactions/confidential_mpt_send.rs @@ -0,0 +1,198 @@ +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + transactions::{Memo, Signer, Transaction, TransactionType}, + Model, ValidateCurrencies, +}; +use crate::models::{FlagCollection, NoFlags}; + +use super::{CommonFields, CommonTransactionBuilder}; + +/// A `ConfidentialMPTSend` transaction transfers a confidential MPT amount +/// from sender to destination, hiding the amount under EC-ElGamal +/// encryption (XLS-0096 §8). The amount is decrypted only by the recipient +/// (and the issuer / optional auditor via their mirror keys). +/// +/// The 946-byte `ZKProof` field carries: +/// - 192 B compact AND-composed sigma proof (ciphertext consistency, +/// Pedersen amount linkage, balance ownership) +/// - 754 B aggregated Bulletproof (range proof on amount AND remainder) +/// +/// `CredentialIDs` (XLS-70) are honored when the destination requires +/// pre-authorization. +#[skip_serializing_none] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] +#[serde(rename_all = "PascalCase")] +pub struct ConfidentialMPTSend<'a> { + #[serde(flatten)] + pub common_fields: CommonFields<'a, NoFlags>, + + /// Destination XRPL account. + pub destination: Cow<'a, str>, + + #[serde(rename = "MPTokenIssuanceID")] + pub mptoken_issuance_id: Cow<'a, str>, + + /// 66-byte ElGamal ciphertext debited from the sender's `CB_S`. + pub sender_encrypted_amount: Cow<'a, str>, + + /// 66-byte ElGamal ciphertext credited to the receiver's `CB_IN`. + pub destination_encrypted_amount: Cow<'a, str>, + + /// 66-byte ElGamal ciphertext used to update both the sender's and + /// receiver's `IssuerEncryptedBalance` mirrors. + pub issuer_encrypted_amount: Cow<'a, str>, + + /// 33-byte Pedersen commitment to the transfer amount. + pub amount_commitment: Cow<'a, str>, + + /// 33-byte Pedersen commitment to the sender's confidential balance. + pub balance_commitment: Cow<'a, str>, + + /// 946-byte composite ZK proof (192 B compact sigma + 754 B aggregated + /// Bulletproof). + #[serde(rename = "ZKProof")] + pub zk_proof: Cow<'a, str>, + + /// 66-byte ciphertext for the auditor mirror. Required iff the + /// issuance has an `AuditorEncryptionKey` registered. + pub auditor_encrypted_amount: Option>, + + /// XLS-70 credentials presented to satisfy the destination's + /// `DepositPreauth` / `AuthorizeCredentials` requirement, if any. + #[serde(rename = "CredentialIDs")] + pub credential_ids: Option>>, +} + +impl<'a> Model for ConfidentialMPTSend<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} + +impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTSend<'a> { + fn get_transaction_type(&self) -> &TransactionType { + self.common_fields.get_transaction_type() + } + + fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { + self.common_fields.get_common_fields() + } + + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + self.common_fields.get_mut_common_fields() + } +} + +impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTSend<'a> { + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + &mut self.common_fields + } + + fn into_self(self) -> Self { + self + } +} + +impl<'a> ConfidentialMPTSend<'a> { + #[allow(clippy::too_many_arguments)] + pub fn new( + account: Cow<'a, str>, + account_txn_id: Option>, + fee: Option>, + last_ledger_sequence: Option, + memos: Option>, + sequence: Option, + signers: Option>, + source_tag: Option, + ticket_sequence: Option, + destination: Cow<'a, str>, + mptoken_issuance_id: Cow<'a, str>, + sender_encrypted_amount: Cow<'a, str>, + destination_encrypted_amount: Cow<'a, str>, + issuer_encrypted_amount: Cow<'a, str>, + amount_commitment: Cow<'a, str>, + balance_commitment: Cow<'a, str>, + zk_proof: Cow<'a, str>, + auditor_encrypted_amount: Option>, + credential_ids: Option>>, + ) -> Self { + Self { + common_fields: CommonFields::new( + account, + TransactionType::ConfidentialMPTSend, + account_txn_id, + fee, + Some(FlagCollection::default()), + last_ledger_sequence, + memos, + None, + sequence, + signers, + None, + source_tag, + ticket_sequence, + None, + ), + destination, + mptoken_issuance_id, + sender_encrypted_amount, + destination_encrypted_amount, + issuer_encrypted_amount, + amount_commitment, + balance_commitment, + zk_proof, + auditor_encrypted_amount, + credential_ids, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize() { + let tx = ConfidentialMPTSend { + common_fields: CommonFields { + account: "rSenderAccount11111111111111111".into(), + transaction_type: TransactionType::ConfidentialMPTSend, + ..Default::default() + }, + destination: "rRecipientAccount111111111111".into(), + mptoken_issuance_id: "610F33".repeat(4).into(), + sender_encrypted_amount: "AD".repeat(66).into(), + destination_encrypted_amount: "DF".repeat(66).into(), + issuer_encrypted_amount: "BC".repeat(66).into(), + amount_commitment: "04".repeat(33).into(), + balance_commitment: "03".repeat(33).into(), + zk_proof: "84".repeat(946).into(), + auditor_encrypted_amount: None, + credential_ids: None, + }; + + let json = serde_json::to_string(&tx).unwrap(); + assert!(json.contains("\"TransactionType\":\"ConfidentialMPTSend\"")); + assert!(json.contains("\"Destination\":\"rRecipientAccount")); + assert!(json.contains("\"AmountCommitment\"")); + assert!(json.contains("\"BalanceCommitment\"")); + assert!(json.contains("\"ZKProof\"")); + + let round_tripped: ConfidentialMPTSend = serde_json::from_str(&json).unwrap(); + assert_eq!(round_tripped, tx); + } +} diff --git a/src/models/transactions/mod.rs b/src/models/transactions/mod.rs index 167d56c8..95e7cbfd 100644 --- a/src/models/transactions/mod.rs +++ b/src/models/transactions/mod.rs @@ -9,6 +9,16 @@ pub mod amm_withdraw; pub mod check_cancel; pub mod check_cash; pub mod check_create; +#[cfg(feature = "confidential-mpt")] +pub mod confidential_mpt_clawback; +#[cfg(feature = "confidential-mpt")] +pub mod confidential_mpt_convert; +#[cfg(feature = "confidential-mpt")] +pub mod confidential_mpt_convert_back; +#[cfg(feature = "confidential-mpt")] +pub mod confidential_mpt_merge_inbox; +#[cfg(feature = "confidential-mpt")] +pub mod confidential_mpt_send; pub mod deposit_preauth; pub mod escrow_cancel; pub mod escrow_create; @@ -76,6 +86,11 @@ pub enum TransactionType { CheckCancel, CheckCash, CheckCreate, + ConfidentialMPTClawback, + ConfidentialMPTConvert, + ConfidentialMPTConvertBack, + ConfidentialMPTMergeInbox, + ConfidentialMPTSend, DepositPreauth, EscrowCancel, EscrowCreate, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1e0cfb6c..7c6e0313 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -262,6 +262,26 @@ where + Clone + core::fmt::Debug, F: strum::IntoEnumIterator + serde::Serialize + core::fmt::Debug + PartialEq + Clone + 'a, +{ + test_transaction_with_result(tx, wallet, "tesSUCCESS").await; +} + +/// Sign, submit, assert the engine result equals `expected_engine_result`, then +/// advance the ledger. Pass `"tesSUCCESS"` for happy paths, or a specific +/// `tec`/`tem`/`tef` code (e.g. `"tecNO_PERMISSION"`) to validate an expected +/// failure case. +pub async fn test_transaction_with_result<'a, T, F>( + tx: &mut T, + wallet: &Wallet, + expected_engine_result: &str, +) where + T: xrpl::models::transactions::Transaction<'a, F> + + xrpl::models::Model + + serde::Serialize + + serde::de::DeserializeOwned + + Clone + + core::fmt::Debug, + F: strum::IntoEnumIterator + serde::Serialize + core::fmt::Debug + PartialEq + Clone + 'a, { use xrpl::asynch::transaction::sign_and_submit; let client = get_client().await; @@ -269,9 +289,9 @@ where .await .expect("test_transaction: sign_and_submit failed"); assert_eq!( - result.engine_result, "tesSUCCESS", - "Expected tesSUCCESS but got: {} — {}", - result.engine_result, result.engine_result_message + result.engine_result, expected_engine_result, + "Expected {} but got: {} — {}", + expected_engine_result, result.engine_result, result.engine_result_message ); ledger_accept().await; } diff --git a/tests/transactions/confidential_mpt.rs b/tests/transactions/confidential_mpt.rs new file mode 100644 index 00000000..712d0e39 --- /dev/null +++ b/tests/transactions/confidential_mpt.rs @@ -0,0 +1,1255 @@ +//! Happy-path integration tests for XLS-0096 ConfidentialMPT transactions +//! against the local standalone node. +//! +//! `ConfidentialMPTConvert` and `ConfidentialMPTMergeInbox` need real ledger +//! state — a confidential-capable `MPTokenIssuance` with a registered +//! `IssuerEncryptionKey`, an authorized holder, and a public MPT balance to +//! convert. xrpl-rust has no models for those prerequisite transactions yet, +//! so [`setup_confidential_issuance`] builds them with **raw JSON-RPC** +//! (`submit` with a `secret`, signed server-side — the node knows +//! `MPTokenIssuanceCreate`/`Set`/`Authorize` and `Payment` from MPTokensV1). +//! Only the transaction *under test* goes through the SDK. + +use crate::common::constants::STANDALONE_URL; +use crate::common::{ + generate_funded_wallet, get_client, ledger_accept, test_transaction, + test_transaction_with_result, with_blockchain_lock, +}; +use xrpl::asynch::account::get_next_valid_seq_number; +use xrpl::asynch::clients::AsyncJsonRpcClient; +use xrpl::models::transactions::confidential_mpt_clawback::ConfidentialMPTClawback; +use xrpl::models::transactions::confidential_mpt_convert::ConfidentialMPTConvert; +use xrpl::models::transactions::confidential_mpt_convert_back::ConfidentialMPTConvertBack; +use xrpl::models::transactions::confidential_mpt_merge_inbox::ConfidentialMPTMergeInbox; +use xrpl::models::transactions::confidential_mpt_send::ConfidentialMPTSend; +use xrpl::mpt_crypto::{Privkey, Pubkey}; +use xrpl::wallet::Wallet; + +// Fee for the prerequisite MPT transactions. None of these transactors +// override `calculateBaseFee`, so the node's 200-drop reference fee covers them. +const MPT_TXN_FEE: &str = "200"; +// Public MPT balance the issuer funds each holder with (something to convert). +const FUNDED_MPT: &str = "1000"; +// MPTokenIssuance create-time flags +const TF_MPT_CAN_LOCK: u32 = 0x0000_0002; +const TF_MPT_REQUIRE_AUTH: u32 = 0x0000_0004; +const TF_MPT_CAN_TRANSFER: u32 = 0x0000_0020; +const TF_MPT_CAN_CLAWBACK: u32 = 0x0000_0040; +const TF_MPT_CAN_CONFIDENTIAL_AMOUNT: u32 = 0x0000_0080; +// MPTokenIssuanceSet flag: lock the whole issuance, or a single holder's token. +const TF_MPT_LOCK: u32 = 0x0000_0001; +// MPTokenAuthorize flag: issuer revokes a holder's authorization. +const TF_MPT_UNAUTHORIZE: u32 = 0x0000_0001; + +// ───────────────────────────────────────────────────────────────────────── +// Raw JSON-RPC helpers (no SDK models needed for the prerequisites) +// ───────────────────────────────────────────────────────────────────────── + +/// Uppercase-hex encode bytes for the transaction's hex string fields. +fn uppercase_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02X}")).collect() +} + +/// Decode a hex string into bytes (e.g. the 24-byte MPTokenIssuanceID). +fn hex_to_bytes(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +/// Decode an XRPL classic address into its 20-byte AccountID. +fn account_id_bytes(address: &str) -> [u8; 20] { + xrpl::core::addresscodec::decode_classic_address(address) + .expect("decode classic address") + .try_into() + .expect("20-byte AccountID") +} + +/// Decode a hex `MPTokenIssuanceID` into its 24 bytes. +fn issuance_id_bytes(issuance_id: &str) -> [u8; 24] { + hex_to_bytes(issuance_id) + .try_into() + .expect("24-byte MPTokenIssuanceID") +} + +/// POST a JSON-RPC request to the standalone node and return `result`. +async fn rpc(body: serde_json::Value) -> serde_json::Value { + let resp: serde_json::Value = reqwest::Client::new() + .post(STANDALONE_URL) + .json(&body) + .send() + .await + .expect("rpc request") + .json() + .await + .expect("rpc json"); + resp["result"].clone() +} + +/// Submit a server-signed `tx_json` (`secret` = wallet seed), assert +/// `tesSUCCESS`, and advance the ledger. Used only for prerequisite setup. +async fn submit_signed(seed: &str, tx_json: serde_json::Value) { + let tx_type = tx_json["TransactionType"] + .as_str() + .unwrap_or("?") + .to_string(); + let result = rpc(serde_json::json!({ + "method": "submit", + "params": [{ "secret": seed, "tx_json": tx_json }], + })) + .await; + let code = result["engine_result"].as_str().unwrap_or(""); + assert_eq!( + code, + "tesSUCCESS", + "prerequisite {tx_type} failed: {code} — {}", + result["engine_result_message"].as_str().unwrap_or("") + ); + ledger_accept().await; +} + +/// The `mpt_issuance_id` of the (single) issuance owned by `account`. +async fn sole_issuance_id(account: &str) -> String { + let result = rpc(serde_json::json!({ + "method": "account_objects", + "params": [{ "account": account, "type": "mpt_issuance", "ledger_index": "validated" }], + })) + .await; + result["account_objects"][0]["mpt_issuance_id"] + .as_str() + .expect("mpt_issuance_id present") + .to_string() +} + +/// The holder's `MPToken` ledger object for `issuance_id`. +async fn holder_mptoken(holder: &str, issuance_id: &str) -> serde_json::Value { + let result = rpc(serde_json::json!({ + "method": "account_objects", + "params": [{ "account": holder, "type": "mptoken", "ledger_index": "validated" }], + })) + .await; + result["account_objects"] + .as_array() + .into_iter() + .flatten() + .find(|o| o["MPTokenIssuanceID"].as_str() == Some(issuance_id)) + .cloned() + .expect("holder MPToken for issuance") +} + +/// The holder's public (unencrypted) MPT balance for `issuance_id` (0 if absent). +async fn holder_public_mpt_balance(holder: &str, issuance_id: &str) -> u64 { + let amount = holder_mptoken(holder, issuance_id).await["MPTAmount"].clone(); + amount + .as_u64() + .or_else(|| amount.as_str().and_then(|s| s.parse().ok())) + .unwrap_or(0) +} + +/// Decrypt a hex-encoded 66-byte ElGamal confidential balance with the holder's +/// secret key, recovering the plaintext amount. +fn decrypt_confidential_balance(hex_ciphertext: &str, holder_sk: &Privkey) -> u64 { + use xrpl::mpt_crypto::{encrypt, Ciphertext}; + let bytes: [u8; 66] = hex_to_bytes(hex_ciphertext) + .try_into() + .expect("66-byte ElGamal ciphertext"); + encrypt::decrypt(&Ciphertext::new(bytes), holder_sk).expect("decrypt confidential balance") +} + +/// The holder's on-ledger spending ciphertext (`ConfidentialBalanceSpending`, +/// 66 bytes) and confidential balance version — the inputs a ConvertBack/Send +/// proof must bind to. +async fn onledger_spending(setup: &ConfidentialSetup) -> ([u8; 66], u32) { + let mptoken = holder_mptoken(&setup.holder.classic_address, &setup.issuance_id).await; + let spending: [u8; 66] = hex_to_bytes( + mptoken["ConfidentialBalanceSpending"] + .as_str() + .expect("ConfidentialBalanceSpending present"), + ) + .try_into() + .expect("66-byte spending ciphertext"); + let version = mptoken["ConfidentialBalanceVersion"].as_u64().unwrap_or(0) as u32; + (spending, version) +} + +// ───────────────────────────────────────────────────────────────────────── +// Prerequisite ledger state for a confidential MPT +// ───────────────────────────────────────────────────────────────────────── + +/// Everything a holder needs to make a successful first `ConfidentialMPTConvert`: +/// a funded holder account that is authorized on a confidential-capable +/// issuance (whose `IssuerEncryptionKey` is `issuer_elgamal_pk`) and holds a +/// public MPT balance. The holder's ElGamal keypair is generated here too. +struct ConfidentialSetup { + issuer: Wallet, + holder: Wallet, + issuance_id: String, + issuer_elgamal_sk: Privkey, + issuer_elgamal_pk: Pubkey, + holder_elgamal_sk: Privkey, + holder_elgamal_pk: Pubkey, +} + +/// Build the prerequisite ledger state via raw JSON-RPC: +/// `MPTokenIssuanceCreate` → `MPTokenIssuanceSet` (register IssuerEncryptionKey) +/// → `MPTokenAuthorize` → `Payment` (public MPT balance to the holder). +async fn setup_confidential_issuance(issuance_flags: u32) -> ConfidentialSetup { + use xrpl::mpt_crypto::keypair; + + // Issuer and holder are distinct funded accounts (Convert requires the + // converting account != issuer). Their seeds let us sign server-side. + let issuer = generate_funded_wallet().await; + let holder = generate_funded_wallet().await; + + // ElGamal encryption keypairs (separate from the accounts' signing keys). + let (issuer_elgamal_sk, issuer_elgamal_pk) = + keypair::generate().expect("issuer ElGamal keypair"); + let (holder_elgamal_sk, holder_elgamal_pk) = + keypair::generate().expect("holder ElGamal keypair"); + + // 1. Issuer creates the issuance with the requested capabilities (caller + // sets the flags; no TransferFee allowed alongside the confidential one). + submit_signed( + &issuer.seed, + serde_json::json!({ + "TransactionType": "MPTokenIssuanceCreate", + "Account": issuer.classic_address, + "Flags": issuance_flags, + "AssetScale": 0, + "MaximumAmount": "1000000000", + "Fee": MPT_TXN_FEE, + }), + ) + .await; + let issuance_id = sole_issuance_id(&issuer.classic_address).await; + + // 2. Issuer registers its ElGamal public key on the issuance. + submit_signed( + &issuer.seed, + serde_json::json!({ + "TransactionType": "MPTokenIssuanceSet", + "Account": issuer.classic_address, + "MPTokenIssuanceID": issuance_id, + "IssuerEncryptionKey": uppercase_hex(issuer_elgamal_pk.as_bytes()), + "Fee": MPT_TXN_FEE, + }), + ) + .await; + + // 3. Holder opts in to the issuance. + submit_signed( + &holder.seed, + serde_json::json!({ + "TransactionType": "MPTokenAuthorize", + "Account": holder.classic_address, + "MPTokenIssuanceID": issuance_id, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + + // 3b. Under RequireAuth, the issuer must also authorize the holder before + // the holder can hold or convert the token. + if issuance_flags & TF_MPT_REQUIRE_AUTH != 0 { + submit_signed( + &issuer.seed, + serde_json::json!({ + "TransactionType": "MPTokenAuthorize", + "Account": issuer.classic_address, + "Holder": holder.classic_address, + "MPTokenIssuanceID": issuance_id, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + } + + // 4. Issuer sends the holder a public MPT balance to convert. + submit_signed( + &issuer.seed, + serde_json::json!({ + "TransactionType": "Payment", + "Account": issuer.classic_address, + "Destination": holder.classic_address, + "Amount": { "mpt_issuance_id": issuance_id, "value": FUNDED_MPT }, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + + ConfidentialSetup { + issuer, + holder, + issuance_id, + issuer_elgamal_sk, + issuer_elgamal_pk, + holder_elgamal_sk, + holder_elgamal_pk, + } +} + +// ───────────────────────────────────────────────────────────────────────── +// ConfidentialMPTConvert crypto (real mpt-crypto material) +// ───────────────────────────────────────────────────────────────────────── + +/// Hex-encoded crypto fields for a first (registration) `ConfidentialMPTConvert`. +struct ConfidentialMPTConvertBundle { + holder_encrypted_amount: String, + issuer_encrypted_amount: String, + blinding_factor: String, + holder_encryption_key: String, + zk_proof: String, +} + +/// Build the Convert crypto bound to the real issuance + holder sequence. +/// +/// The amount is encrypted under both the holder's key and the issuance's +/// **registered** `IssuerEncryptionKey` with the same revealed blinding factor; +/// the Schnorr `ZKProof` proves knowledge of the holder's secret key, bound via +/// the context hash to (holder account, issuance id, sequence) — exactly what +/// rippled's preclaim verifies. +fn build_convert_material( + holder_account: &str, + issuance_id_hex: &str, + sequence: u32, + amount: u64, + issuer_elgamal_pk: &Pubkey, + holder_elgamal_sk: &Privkey, + holder_elgamal_pk: &Pubkey, +) -> ConfidentialMPTConvertBundle { + use xrpl::mpt_crypto::{context, encrypt, prove, AccountId, IssuanceId}; + + let r = encrypt::random_blinding_factor().expect("blinding factor"); + let holder_ct = encrypt::encrypt(amount, holder_elgamal_pk, &r).expect("holder ciphertext"); + let issuer_ct = encrypt::encrypt(amount, issuer_elgamal_pk, &r).expect("issuer ciphertext"); + + let account = account_id_bytes(holder_account); + let issuance = issuance_id_bytes(issuance_id_hex); + let ctx = context::convert( + &AccountId::new(account), + &IssuanceId::new(issuance), + sequence, + ) + .expect("convert context hash"); + let proof = prove::convert(holder_elgamal_sk, holder_elgamal_pk, &ctx).expect("convert proof"); + + ConfidentialMPTConvertBundle { + holder_encrypted_amount: uppercase_hex(holder_ct.as_bytes()), + issuer_encrypted_amount: uppercase_hex(issuer_ct.as_bytes()), + blinding_factor: uppercase_hex(r.as_bytes()), + holder_encryption_key: uppercase_hex(holder_elgamal_pk.as_bytes()), + zk_proof: uppercase_hex(proof.as_bytes()), + } +} + +/// Submit a first (registration) `ConfidentialMPTConvert` for `wallet` and +/// assert `tesSUCCESS`. Generic over the holder so it serves both the primary +/// holder and a Send destination. +#[allow(clippy::too_many_arguments)] +async fn submit_first_convert( + client: &AsyncJsonRpcClient, + wallet: &Wallet, + holder_sk: &Privkey, + holder_pk: &Pubkey, + issuer_pk: &Pubkey, + issuance_id: &str, + amount: u64, +) { + // The proof binds to the exact sequence the transaction carries. + let sequence = get_next_valid_seq_number(wallet.classic_address.clone().into(), client, None) + .await + .expect("fetch holder sequence"); + let m = build_convert_material( + &wallet.classic_address, + issuance_id, + sequence, + amount, + issuer_pk, + holder_sk, + holder_pk, + ); + + let mut tx = ConfidentialMPTConvert::new( + wallet.classic_address.clone().into(), + None, // account_txn_id + None, // fee — autofilled (cMPT = 10× base) + None, // last_ledger_sequence + None, // memos + Some(sequence), // bound into the proof context above + None, // signers + None, // source_tag + None, // ticket_sequence + issuance_id.to_string().into(), + amount.to_string().into(), + m.holder_encrypted_amount.into(), + m.issuer_encrypted_amount.into(), + m.blinding_factor.into(), + Some(m.holder_encryption_key.into()), // first Convert registers the key + None, // AuditorEncryptedAmount (no auditor) + Some(m.zk_proof.into()), // first Convert carries the proof + ); + + test_transaction(&mut tx, wallet).await; +} + +/// The primary holder's first Convert (the common case). +async fn convert_public_to_confidential( + setup: &ConfidentialSetup, + client: &AsyncJsonRpcClient, + amount: u64, +) { + submit_first_convert( + client, + &setup.holder, + &setup.holder_elgamal_sk, + &setup.holder_elgamal_pk, + &setup.issuer_elgamal_pk, + &setup.issuance_id, + amount, + ) + .await; +} + +// ───────────────────────────────────────────────────────────────────────── +// Happy-path tests +// ───────────────────────────────────────────────────────────────────────── + +/// First (registration) `ConfidentialMPTConvert`: converts part of the holder's +/// public MPT balance into confidential form. SDK-built; expects `tesSUCCESS` +/// and verifies the public balance is debited by exactly the converted amount. +#[tokio::test] +async fn confidential_mpt_convert() { + with_blockchain_lock(|| async { + let setup = setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT).await; + let client = get_client().await; + + let amount: u64 = 100; + let before = + holder_public_mpt_balance(&setup.holder.classic_address, &setup.issuance_id).await; + convert_public_to_confidential(&setup, client, amount).await; + let after = + holder_public_mpt_balance(&setup.holder.classic_address, &setup.issuance_id).await; + assert_eq!( + after, + before - amount, + "Convert should debit {amount} from the public balance ({before} → {after})" + ); + + // The holder's confidential state is now initialized: the ElGamal key is + // registered, and the encrypted inbox decrypts (with the holder's secret + // key) to exactly the converted amount. + let mptoken = holder_mptoken(&setup.holder.classic_address, &setup.issuance_id).await; + let registered_key = uppercase_hex(setup.holder_elgamal_pk.as_bytes()); + assert_eq!( + mptoken["HolderEncryptionKey"].as_str(), + Some(registered_key.as_str()), + "HolderEncryptionKey should be the registered holder key" + ); + let inbox = decrypt_confidential_balance( + mptoken["ConfidentialBalanceInbox"] + .as_str() + .expect("ConfidentialBalanceInbox present"), + &setup.holder_elgamal_sk, + ); + assert_eq!( + inbox, amount, + "confidential inbox should decrypt to {amount}" + ); + }) + .await; +} + +/// `ConfidentialMPTMergeInbox` after a Convert: the Convert deposits into the +/// holder's confidential inbox, then MergeInbox folds it into the spending +/// balance. +#[tokio::test] +async fn confidential_mpt_merge_inbox() { + with_blockchain_lock(|| async { + let setup = setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT).await; + let client = get_client().await; + + // Initialize the holder's confidential state + populate the inbox. + let amount: u64 = 100; + convert_public_to_confidential(&setup, client, amount).await; + merge_confidential_inbox(&setup).await; + + // After merge, the inbox is folded into the spending balance and reset: + // spending decrypts to the converted amount, inbox to zero. + let mptoken = holder_mptoken(&setup.holder.classic_address, &setup.issuance_id).await; + let spending = decrypt_confidential_balance( + mptoken["ConfidentialBalanceSpending"] + .as_str() + .expect("ConfidentialBalanceSpending present"), + &setup.holder_elgamal_sk, + ); + let inbox = decrypt_confidential_balance( + mptoken["ConfidentialBalanceInbox"] + .as_str() + .expect("ConfidentialBalanceInbox present"), + &setup.holder_elgamal_sk, + ); + assert_eq!( + spending, amount, + "spending should decrypt to {amount} after merge" + ); + assert_eq!(inbox, 0, "inbox should decrypt to 0 (reset) after merge"); + // The first Convert leaves the version at 0 (omitted); MergeInbox bumps it. + assert_eq!( + mptoken["ConfidentialBalanceVersion"].as_u64(), + Some(1), + "merge should bump ConfidentialBalanceVersion to 1" + ); + }) + .await; +} + +/// XLS-0096 §9.2.1.2 protocol-level failure #2: `ConfidentialMPTMergeInbox` on +/// an issuance that lacks `lsfMPTCanConfidentialAmount` must be rejected with +/// `tecNO_PERMISSION`. The issuance is created without the confidential flag and +/// the holder is authorized, so the `MPTokenIssuance` and `MPToken` both exist +/// (preclaim check #1 passes) — only the missing-capability check (#2) fires. +#[tokio::test] +async fn confidential_mpt_merge_inbox_rejects_non_confidential_issuance() { + with_blockchain_lock(|| async { + let issuer = generate_funded_wallet().await; + let holder = generate_funded_wallet().await; + + // Plain (non-confidential) issuance: no tfMPTCanConfidentialAmount. + submit_signed( + &issuer.seed, + serde_json::json!({ + "TransactionType": "MPTokenIssuanceCreate", + "Account": issuer.classic_address, + "Flags": 0, + "AssetScale": 0, + "MaximumAmount": "1000000000", + "Fee": MPT_TXN_FEE, + }), + ) + .await; + let issuance_id = sole_issuance_id(&issuer.classic_address).await; + + // Holder opts in, so the MPToken exists (check #1 passes). + submit_signed( + &holder.seed, + serde_json::json!({ + "TransactionType": "MPTokenAuthorize", + "Account": holder.classic_address, + "MPTokenIssuanceID": issuance_id, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + + let mut tx = ConfidentialMPTMergeInbox::new( + holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + issuance_id.clone().into(), + ); + test_transaction_with_result(&mut tx, &holder, "tecNO_PERMISSION").await; + }) + .await; +} + +/// XLS-0096 §9.2.1.2 protocol-level failure #3: `ConfidentialMPTMergeInbox` on a +/// confidential-capable issuance whose holder has not yet Converted must be +/// rejected with `tecNO_PERMISSION`. The holder is authorized (MPToken exists, +/// confidential flag set — checks #1 and #2 pass) but has never Converted, so +/// `ConfidentialBalanceInbox`/`Spending` are absent — the uninitialized check +/// (#3) fires. +#[tokio::test] +async fn confidential_mpt_merge_inbox_rejects_uninitialized_mptoken() { + with_blockchain_lock(|| async { + // Authorizes the holder but performs no Convert — confidential balances + // remain uninitialized. + let setup = setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT).await; + + let mut tx = ConfidentialMPTMergeInbox::new( + setup.holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + setup.issuance_id.clone().into(), + ); + test_transaction_with_result(&mut tx, &setup.holder, "tecNO_PERMISSION").await; + }) + .await; +} + +/// XLS-0096 §9.2.1.2 protocol-level failure #1: `ConfidentialMPTMergeInbox` +/// referencing a non-existent `MPTokenIssuance` must be rejected with +/// `tecOBJECT_NOT_FOUND`. We fabricate a valid-format `MPTokenIssuanceID` whose +/// embedded issuer (bytes 4..24) is a real account distinct from the submitter, +/// so preflight's account-is-not-issuer check passes — but the (sequence, +/// issuer) pair was never used to create an issuance, so the lookup fails. +#[tokio::test] +async fn confidential_mpt_merge_inbox_rejects_missing_issuance() { + with_blockchain_lock(|| async { + let issuer = generate_funded_wallet().await; + let holder = generate_funded_wallet().await; + + // 24-byte MPTokenIssuanceID = 4-byte sequence ++ 20-byte issuer AccountID. + let mut id = [0u8; 24]; + id[..4].copy_from_slice(&1u32.to_be_bytes()); + id[4..].copy_from_slice(&account_id_bytes(&issuer.classic_address)); + let missing_issuance_id = uppercase_hex(&id); + + let mut tx = ConfidentialMPTMergeInbox::new( + holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + missing_issuance_id.into(), + ); + test_transaction_with_result(&mut tx, &holder, "tecOBJECT_NOT_FOUND").await; + }) + .await; +} + +/// XLS-0096 §9.2.1.2 protocol-level failure #4: `ConfidentialMPTMergeInbox` by a +/// holder whose authorization has been revoked must be rejected with +/// `tecNO_AUTH`. rippled checks auth *last*, so the holder must first reach a +/// valid state (authorized + Converted) before the issuer unauthorizes it. +#[tokio::test] +async fn confidential_mpt_merge_inbox_rejects_unauthorized_holder() { + with_blockchain_lock(|| async { + let setup = + setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT | TF_MPT_REQUIRE_AUTH).await; + let client = get_client().await; + + // Initialize the holder's confidential balances (passes checks #1–#3). + convert_public_to_confidential(&setup, client, 50).await; + + // Issuer revokes the holder's authorization. + submit_signed( + &setup.issuer.seed, + serde_json::json!({ + "TransactionType": "MPTokenAuthorize", + "Account": setup.issuer.classic_address, + "Holder": setup.holder.classic_address, + "MPTokenIssuanceID": setup.issuance_id, + "Flags": TF_MPT_UNAUTHORIZE, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + + let mut tx = ConfidentialMPTMergeInbox::new( + setup.holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + setup.issuance_id.clone().into(), + ); + test_transaction_with_result(&mut tx, &setup.holder, "tecNO_AUTH").await; + }) + .await; +} + +/// XLS-0096 §9.2.1.2 protocol-level failure #5: `ConfidentialMPTMergeInbox` on a +/// holder whose token has been individually locked must be rejected with +/// `tecLOCKED`. The holder Converts first (passes #1–#3); rippled's frozen check +/// runs before the auth check. +#[tokio::test] +async fn confidential_mpt_merge_inbox_rejects_locked_holder() { + with_blockchain_lock(|| async { + let setup = + setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT | TF_MPT_CAN_LOCK).await; + let client = get_client().await; + + convert_public_to_confidential(&setup, client, 50).await; + + // Issuer locks the holder's MPToken (Holder field present). + submit_signed( + &setup.issuer.seed, + serde_json::json!({ + "TransactionType": "MPTokenIssuanceSet", + "Account": setup.issuer.classic_address, + "MPTokenIssuanceID": setup.issuance_id, + "Holder": setup.holder.classic_address, + "Flags": TF_MPT_LOCK, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + + let mut tx = ConfidentialMPTMergeInbox::new( + setup.holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + setup.issuance_id.clone().into(), + ); + test_transaction_with_result(&mut tx, &setup.holder, "tecLOCKED").await; + }) + .await; +} + +/// XLS-0096 §9.2.1.2 protocol-level failure #6: `ConfidentialMPTMergeInbox` while +/// the entire issuance is locked must be rejected with `tecLOCKED`. Same as #5 +/// but the issuer locks the issuance globally (no `Holder` field). +#[tokio::test] +async fn confidential_mpt_merge_inbox_rejects_locked_issuance() { + with_blockchain_lock(|| async { + let setup = + setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT | TF_MPT_CAN_LOCK).await; + let client = get_client().await; + + convert_public_to_confidential(&setup, client, 50).await; + + // Issuer locks the entire issuance (no Holder field → global lock). + submit_signed( + &setup.issuer.seed, + serde_json::json!({ + "TransactionType": "MPTokenIssuanceSet", + "Account": setup.issuer.classic_address, + "MPTokenIssuanceID": setup.issuance_id, + "Flags": TF_MPT_LOCK, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + + let mut tx = ConfidentialMPTMergeInbox::new( + setup.holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + setup.issuance_id.clone().into(), + ); + test_transaction_with_result(&mut tx, &setup.holder, "tecLOCKED").await; + }) + .await; +} + +/// XLS-0096 §9.2.1.2 protocol-level failure #7: the issuer attempting to merge +/// its own issuance. +/// +/// The spec frames this as a `tefINTERNAL` invariant, but rippled's preclaim +/// check for it is an unreachable defensive backstop (marked `LCOV_EXCL_LINE`): +/// preflight already rejects an issuer-submitted merge with `temMALFORMED` +/// ("issuer cannot merge"), so `temMALFORMED` is the *observable* result. We +/// assert the real behavior rather than the unreachable invariant code. +#[tokio::test] +async fn confidential_mpt_merge_inbox_rejects_issuer_as_merger() { + with_blockchain_lock(|| async { + let setup = setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT).await; + + // Account == the issuance's embedded issuer, tripping preflight's + // "issuer cannot merge" check before any ledger state is consulted. + let mut tx = ConfidentialMPTMergeInbox::new( + setup.issuer.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + setup.issuance_id.clone().into(), + ); + test_transaction_with_result(&mut tx, &setup.issuer, "temMALFORMED").await; + }) + .await; +} + +/// `ConfidentialMPTClawback`: the issuer claws back the holder's confidential +/// balance. After a Convert the issuer's mirror of the holder's balance equals +/// the converted amount; the issuer reveals it, proves the mirror ciphertext +/// decrypts to it, and the holder's confidential balances are zeroed. +#[tokio::test] +async fn confidential_mpt_clawback() { + with_blockchain_lock(|| async { + let setup = + setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT | TF_MPT_CAN_CLAWBACK).await; + let client = get_client().await; + + let amount: u64 = 100; + // Seed the holder's confidential balance + the issuer's mirror of it. + convert_public_to_confidential(&setup, client, amount).await; + + // The clawback proof binds to (issuer, issuance, sequence, holder); set + // the issuer's sequence explicitly so it matches the proof context. + let sequence = + get_next_valid_seq_number(setup.issuer.classic_address.clone().into(), client, None) + .await + .expect("fetch issuer sequence"); + let zk_proof = build_clawback_proof(&setup, sequence, amount).await; + + let mut tx = ConfidentialMPTClawback::new( + setup.issuer.classic_address.clone().into(), + None, // account_txn_id + None, // fee — autofilled (cMPT = 10× base) + None, // last_ledger_sequence + None, // memos + Some(sequence), // bound into the proof context above + None, // signers + None, // source_tag + None, // ticket_sequence + setup.holder.classic_address.clone().into(), + setup.issuance_id.clone().into(), + amount.to_string().into(), + zk_proof.into(), + ); + + test_transaction(&mut tx, &setup.issuer).await; + + // The holder's confidential balances are zeroed (decrypt to 0). + let mptoken = holder_mptoken(&setup.holder.classic_address, &setup.issuance_id).await; + let inbox = decrypt_confidential_balance( + mptoken["ConfidentialBalanceInbox"] + .as_str() + .expect("ConfidentialBalanceInbox present"), + &setup.holder_elgamal_sk, + ); + assert_eq!( + inbox, 0, + "clawback should zero the holder's confidential inbox" + ); + }) + .await; +} + +/// Build the issuer's 64-byte Clawback proof: reveals `amount` and proves the +/// issuer's mirror of the holder's balance (`IssuerEncryptedBalance`, read from +/// the ledger) decrypts to it under the issuer's ElGamal key. +async fn build_clawback_proof(setup: &ConfidentialSetup, sequence: u32, amount: u64) -> String { + use xrpl::mpt_crypto::{context, prove, AccountId, Ciphertext, IssuanceId}; + + let mptoken = holder_mptoken(&setup.holder.classic_address, &setup.issuance_id).await; + let issuer_amount_mirror: [u8; 66] = hex_to_bytes( + mptoken["IssuerEncryptedBalance"] + .as_str() + .expect("IssuerEncryptedBalance present"), + ) + .try_into() + .expect("66-byte issuer-mirror ciphertext"); + + let issuer_account = account_id_bytes(&setup.issuer.classic_address); + let holder_account = account_id_bytes(&setup.holder.classic_address); + let issuance = issuance_id_bytes(&setup.issuance_id); + + let ctx = context::clawback( + &AccountId::new(issuer_account), + &IssuanceId::new(issuance), + sequence, + &AccountId::new(holder_account), + ) + .expect("clawback context hash"); + let proof = prove::clawback( + &setup.issuer_elgamal_sk, + &setup.issuer_elgamal_pk, + &ctx, + amount, + &Ciphertext::new(issuer_amount_mirror), + ) + .expect("clawback proof"); + uppercase_hex(proof.as_bytes()) +} + +/// Submit the holder's `ConfidentialMPTMergeInbox` via the SDK and assert +/// `tesSUCCESS`. Moves the confidential inbox into the spending balance. +async fn merge_confidential_inbox(setup: &ConfidentialSetup) { + let mut tx = ConfidentialMPTMergeInbox::new( + setup.holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + setup.issuance_id.clone().into(), + ); + test_transaction(&mut tx, &setup.holder).await; +} + +/// `ConfidentialMPTConvertBack`: the holder withdraws confidential balance back +/// to public. Requires a *spending* balance (Convert + MergeInbox), then proves +/// (balance − amount) ≥ 0 against the on-ledger spending ciphertext. SDK-built; +/// expects `tesSUCCESS` and verifies the public credit + zeroed spending balance. +#[tokio::test] +async fn confidential_mpt_convert_back() { + with_blockchain_lock(|| async { + let setup = setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT).await; + let client = get_client().await; + + let amount: u64 = 100; + // Convert + merge so the full amount sits in the spending balance. + convert_public_to_confidential(&setup, client, amount).await; + merge_confidential_inbox(&setup).await; + + let public_before = + holder_public_mpt_balance(&setup.holder.classic_address, &setup.issuance_id).await; + + // Withdraw the entire spending balance back to public. + let sequence = + get_next_valid_seq_number(setup.holder.classic_address.clone().into(), client, None) + .await + .expect("fetch holder sequence"); + let m = build_convert_back_material(&setup, sequence, amount, amount).await; + + let mut tx = ConfidentialMPTConvertBack::new( + setup.holder.classic_address.clone().into(), + None, // account_txn_id + None, // fee — autofilled (cMPT = 10× base) + None, // last_ledger_sequence + None, // memos + Some(sequence), // bound into the proof context above + None, // signers + None, // source_tag + None, // ticket_sequence + setup.issuance_id.clone().into(), + amount.to_string().into(), + m.holder_encrypted_amount.into(), + m.issuer_encrypted_amount.into(), + m.blinding_factor.into(), + m.balance_commitment.into(), + m.zk_proof.into(), + None, // auditor_encrypted_amount (no auditor) + ); + + test_transaction(&mut tx, &setup.holder).await; + + // Public balance credited by the withdrawn amount; spending → 0. + let public_after = + holder_public_mpt_balance(&setup.holder.classic_address, &setup.issuance_id).await; + assert_eq!( + public_after, + public_before + amount, + "ConvertBack should credit {amount} to the public balance ({public_before} → {public_after})" + ); + let mptoken = holder_mptoken(&setup.holder.classic_address, &setup.issuance_id).await; + let spending = decrypt_confidential_balance( + mptoken["ConfidentialBalanceSpending"] + .as_str() + .expect("ConfidentialBalanceSpending present"), + &setup.holder_elgamal_sk, + ); + assert_eq!(spending, 0, "spending should decrypt to 0 after converting it all back"); + }) + .await; +} + +/// Hex-encoded crypto fields for a `ConfidentialMPTConvertBack`. +struct ConfidentialMPTConvertBackBundle { + holder_encrypted_amount: String, + issuer_encrypted_amount: String, + blinding_factor: String, + balance_commitment: String, + zk_proof: String, +} + +/// Build the ConvertBack crypto: the revealed amount (encrypted under holder + +/// issuer keys), a Pedersen commitment to the current spending balance, and the +/// 816-byte proof linking that commitment to the on-ledger spending ciphertext +/// (read here) while range-proving `current_balance − amount ≥ 0`. The context +/// binds to the holder + issuance + sequence + on-ledger balance version. +async fn build_convert_back_material( + setup: &ConfidentialSetup, + sequence: u32, + amount: u64, + current_balance: u64, +) -> ConfidentialMPTConvertBackBundle { + use xrpl::mpt_crypto::{commit, context, encrypt, prove, AccountId, Ciphertext, IssuanceId}; + + let r = encrypt::random_blinding_factor().expect("blinding factor"); + let holder_ct = encrypt::encrypt(amount, &setup.holder_elgamal_pk, &r).expect("holder ct"); + let issuer_ct = encrypt::encrypt(amount, &setup.issuer_elgamal_pk, &r).expect("issuer ct"); + + let balance_blinding = encrypt::random_blinding_factor().expect("balance blinding"); + let balance_commitment = + commit::pedersen(current_balance, &balance_blinding).expect("balance commitment"); + + let (cb_s, version) = onledger_spending(setup).await; + let holder_account = account_id_bytes(&setup.holder.classic_address); + let issuance = issuance_id_bytes(&setup.issuance_id); + let ctx = context::convert_back( + &AccountId::new(holder_account), + &IssuanceId::new(issuance), + sequence, + version, + ) + .expect("convert_back context hash"); + + let proof = prove::convert_back(prove::ConvertBackProofParams { + holder_privkey: &setup.holder_elgamal_sk, + holder_pubkey: &setup.holder_elgamal_pk, + amount, + current_balance, + context_hash: &ctx, + balance_commitment: &balance_commitment, + balance_blinding: &balance_blinding, + balance_ciphertext: &Ciphertext::new(cb_s), + }) + .expect("convert_back proof"); + + ConfidentialMPTConvertBackBundle { + holder_encrypted_amount: uppercase_hex(holder_ct.as_bytes()), + issuer_encrypted_amount: uppercase_hex(issuer_ct.as_bytes()), + blinding_factor: uppercase_hex(r.as_bytes()), + balance_commitment: uppercase_hex(balance_commitment.as_bytes()), + zk_proof: uppercase_hex(proof.as_bytes()), + } +} + +/// Set up a Send destination: a funded, authorized holder that has done a +/// Convert (so it has a registered `HolderEncryptionKey` and a confidential +/// inbox to receive into). Returns the wallet + its ElGamal keypair. +async fn setup_send_destination( + setup: &ConfidentialSetup, + client: &AsyncJsonRpcClient, + seed_amount: u64, +) -> (Wallet, Privkey, Pubkey) { + use xrpl::mpt_crypto::keypair; + + let dest = generate_funded_wallet().await; + let (dest_sk, dest_pk) = keypair::generate().expect("destination ElGamal keypair"); + + submit_signed( + &dest.seed, + serde_json::json!({ + "TransactionType": "MPTokenAuthorize", + "Account": dest.classic_address, + "MPTokenIssuanceID": setup.issuance_id, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + submit_signed( + &setup.issuer.seed, + serde_json::json!({ + "TransactionType": "Payment", + "Account": setup.issuer.classic_address, + "Destination": dest.classic_address, + "Amount": { "mpt_issuance_id": setup.issuance_id, "value": FUNDED_MPT }, + "Fee": MPT_TXN_FEE, + }), + ) + .await; + // A Convert initializes the destination's confidential inbox + key. + submit_first_convert( + client, + &dest, + &dest_sk, + &dest_pk, + &setup.issuer_elgamal_pk, + &setup.issuance_id, + seed_amount, + ) + .await; + + (dest, dest_sk, dest_pk) +} + +/// `ConfidentialMPTSend`: a confidential holder-to-holder transfer. The sender +/// needs a spending balance (Convert + MergeInbox) and the destination an +/// initialized confidential inbox (its own Convert). SDK-built; expects +/// `tesSUCCESS` and verifies the sender is debited and the destination credited. +#[tokio::test] +async fn confidential_mpt_send() { + with_blockchain_lock(|| async { + // Send requires the issuance to allow transfers. + let setup = + setup_confidential_issuance(TF_MPT_CAN_CONFIDENTIAL_AMOUNT | TF_MPT_CAN_TRANSFER).await; + let client = get_client().await; + + // Sender: convert + merge → spending balance. + let sender_balance: u64 = 100; + convert_public_to_confidential(&setup, client, sender_balance).await; + merge_confidential_inbox(&setup).await; + + // Destination: authorized holder with an initialized confidential inbox. + let dest_seed: u64 = 10; + let (dest, dest_sk, dest_pk) = setup_send_destination(&setup, client, dest_seed).await; + + let amount: u64 = 40; + let sequence = + get_next_valid_seq_number(setup.holder.classic_address.clone().into(), client, None) + .await + .expect("fetch sender sequence"); + let m = build_send_material( + &setup, + &dest.classic_address, + &dest_pk, + sequence, + amount, + sender_balance, + ) + .await; + + let mut tx = ConfidentialMPTSend::new( + setup.holder.classic_address.clone().into(), + None, // account_txn_id + None, // fee — autofilled (cMPT = 10× base) + None, // last_ledger_sequence + None, // memos + Some(sequence), // bound into the proof context above + None, // signers + None, // source_tag + None, // ticket_sequence + dest.classic_address.clone().into(), + setup.issuance_id.clone().into(), + m.sender_encrypted_amount.into(), + m.destination_encrypted_amount.into(), + m.issuer_encrypted_amount.into(), + m.amount_commitment.into(), + m.balance_commitment.into(), + m.zk_proof.into(), + None, // auditor_encrypted_amount (no auditor) + None, // credential_ids + ); + + test_transaction(&mut tx, &setup.holder).await; + + // Sender spending debited; destination inbox credited. + let sender_mpt = holder_mptoken(&setup.holder.classic_address, &setup.issuance_id).await; + let sender_spending = decrypt_confidential_balance( + sender_mpt["ConfidentialBalanceSpending"] + .as_str() + .expect("sender ConfidentialBalanceSpending present"), + &setup.holder_elgamal_sk, + ); + assert_eq!( + sender_spending, + sender_balance - amount, + "sender spending should be debited by {amount}" + ); + + let dest_mpt = holder_mptoken(&dest.classic_address, &setup.issuance_id).await; + let dest_inbox = decrypt_confidential_balance( + dest_mpt["ConfidentialBalanceInbox"] + .as_str() + .expect("destination ConfidentialBalanceInbox present"), + &dest_sk, + ); + assert_eq!( + dest_inbox, + dest_seed + amount, + "destination inbox should be credited by {amount}" + ); + }) + .await; +} + +/// Hex-encoded crypto fields for a `ConfidentialMPTSend`. +struct ConfidentialMPTSendBundle { + sender_encrypted_amount: String, + destination_encrypted_amount: String, + issuer_encrypted_amount: String, + amount_commitment: String, + balance_commitment: String, + zk_proof: String, +} + +/// Build the Send crypto. CRITICAL invariant (XLS-0096 §5.4): one shared +/// `tx_blinding_factor` is the ElGamal randomness for *all three* participant +/// ciphertexts AND the Pedersen blinding for `amount_commitment`. The balance +/// commitment uses an independent blinding; the proof links it to the sender's +/// on-ledger spending ciphertext and range-proves `balance − amount ≥ 0`. +async fn build_send_material( + setup: &ConfidentialSetup, + destination_account: &str, + destination_pk: &Pubkey, + sequence: u32, + amount: u64, + current_balance: u64, +) -> ConfidentialMPTSendBundle { + use xrpl::mpt_crypto::{commit, context, encrypt, prove, AccountId, Ciphertext, IssuanceId}; + + // Shared randomness across all ciphertexts + the amount commitment. + let tx_r = encrypt::random_blinding_factor().expect("tx blinding"); + let sender_ct = encrypt::encrypt(amount, &setup.holder_elgamal_pk, &tx_r).expect("sender ct"); + let dest_ct = encrypt::encrypt(amount, destination_pk, &tx_r).expect("dest ct"); + let issuer_ct = encrypt::encrypt(amount, &setup.issuer_elgamal_pk, &tx_r).expect("issuer ct"); + let amount_commitment = commit::pedersen(amount, &tx_r).expect("amount commitment"); + + // Independent blinding for the balance commitment. + let balance_blinding = encrypt::random_blinding_factor().expect("balance blinding"); + let balance_commitment = + commit::pedersen(current_balance, &balance_blinding).expect("balance commitment"); + + let (cb_s, version) = onledger_spending(setup).await; + let sender_account = account_id_bytes(&setup.holder.classic_address); + let dest_account = account_id_bytes(destination_account); + let issuance = issuance_id_bytes(&setup.issuance_id); + let ctx = context::send( + &AccountId::new(sender_account), + &IssuanceId::new(issuance), + sequence, + &AccountId::new(dest_account), + version, + ) + .expect("send context hash"); + + let proof = prove::send(prove::SendProofParams { + sender_privkey: &setup.holder_elgamal_sk, + sender_pubkey: &setup.holder_elgamal_pk, + amount, + current_balance, + tx_blinding_factor: &tx_r, + context_hash: &ctx, + amount_commitment: &amount_commitment, + balance_commitment: &balance_commitment, + balance_blinding: &balance_blinding, + balance_ciphertext: &Ciphertext::new(cb_s), + sender: prove::Participant { + pubkey: &setup.holder_elgamal_pk, + ciphertext: &sender_ct, + }, + destination: prove::Participant { + pubkey: destination_pk, + ciphertext: &dest_ct, + }, + issuer: prove::Participant { + pubkey: &setup.issuer_elgamal_pk, + ciphertext: &issuer_ct, + }, + auditor: None, + }) + .expect("send proof"); + + ConfidentialMPTSendBundle { + sender_encrypted_amount: uppercase_hex(sender_ct.as_bytes()), + destination_encrypted_amount: uppercase_hex(dest_ct.as_bytes()), + issuer_encrypted_amount: uppercase_hex(issuer_ct.as_bytes()), + amount_commitment: uppercase_hex(amount_commitment.as_bytes()), + balance_commitment: uppercase_hex(balance_commitment.as_bytes()), + zk_proof: uppercase_hex(proof.as_bytes()), + } +} diff --git a/tests/transactions/mod.rs b/tests/transactions/mod.rs index 487494ae..1dd35f97 100644 --- a/tests/transactions/mod.rs +++ b/tests/transactions/mod.rs @@ -9,6 +9,8 @@ pub mod amm_withdraw; pub mod check_cancel; pub mod check_cash; pub mod check_create; +#[cfg(feature = "confidential-mpt")] +pub mod confidential_mpt; pub mod deposit_preauth; pub mod escrow_cancel; pub mod escrow_create; diff --git a/xrpl-rust-macros/src/lib.rs b/xrpl-rust-macros/src/lib.rs index 74b337b8..71fc1737 100644 --- a/xrpl-rust-macros/src/lib.rs +++ b/xrpl-rust-macros/src/lib.rs @@ -42,27 +42,25 @@ pub fn derive_validate_currencies(input: TokenStream) -> TokenStream { // Extract T from Option if let syn::PathArguments::AngleBracketed(angle_bracketed) = &segments[0].arguments - { - if let Some(syn::GenericArgument::Type(Type::Path(inner_type_path))) = + && let Some(syn::GenericArgument::Type(Type::Path(inner_type_path))) = angle_bracketed.args.first() + { + let inner_ident = &inner_type_path.path.segments.last().unwrap().ident; + if [ + "Amount", + "XRPAmount", + "IssuedCurrencyAmount", + "Currency", + "XRP", + "IssuedCurrency", + ] + .contains(&inner_ident.to_string().as_str()) { - let inner_ident = &inner_type_path.path.segments.last().unwrap().ident; - if [ - "Amount", - "XRPAmount", - "IssuedCurrencyAmount", - "Currency", - "XRP", - "IssuedCurrency", - ] - .contains(&inner_ident.to_string().as_str()) - { - return Some(quote! { - if let Some(x) = &self.#ident { - x.validate()?; - } - }); - } + return Some(quote! { + if let Some(x) = &self.#ident { + x.validate()?; + } + }); } } }