mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Remove monero-serai, migrating to monero-oxide
This commit is contained in:
72
.github/workflows/monero-tests.yaml
vendored
72
.github/workflows/monero-tests.yaml
vendored
@@ -1,72 +0,0 @@
|
|||||||
name: Monero Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "networks/monero/**"
|
|
||||||
- "processor/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "networks/monero/**"
|
|
||||||
- "processor/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Only run these once since they will be consistent regardless of any node
|
|
||||||
unit-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
|
|
||||||
- name: Run Unit Tests Without Features
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-io --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-generators --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-primitives --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-mlsag --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-clsag --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-borromean --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-bulletproofs --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-rpc --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-address --lib
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --lib
|
|
||||||
|
|
||||||
# Doesn't run unit tests with features as the tests workflow will
|
|
||||||
|
|
||||||
integration-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Test against all supported protocol versions
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
version: [v0.17.3.2, v0.18.3.4]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
with:
|
|
||||||
monero-version: ${{ matrix.version }}
|
|
||||||
|
|
||||||
- name: Run Integration Tests Without Features
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --test '*'
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --test '*'
|
|
||||||
|
|
||||||
- name: Run Integration Tests
|
|
||||||
# Don't run if the the tests workflow also will
|
|
||||||
if: ${{ matrix.version != 'v0.18.3.4' }}
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --all-features --test '*'
|
|
||||||
13
.github/workflows/networks-tests.yml
vendored
13
.github/workflows/networks-tests.yml
vendored
@@ -33,16 +33,3 @@ jobs:
|
|||||||
-p alloy-simple-request-transport \
|
-p alloy-simple-request-transport \
|
||||||
-p ethereum-serai \
|
-p ethereum-serai \
|
||||||
-p serai-ethereum-relayer \
|
-p serai-ethereum-relayer \
|
||||||
-p monero-io \
|
|
||||||
-p monero-generators \
|
|
||||||
-p monero-primitives \
|
|
||||||
-p monero-mlsag \
|
|
||||||
-p monero-clsag \
|
|
||||||
-p monero-borromean \
|
|
||||||
-p monero-bulletproofs \
|
|
||||||
-p monero-serai \
|
|
||||||
-p monero-rpc \
|
|
||||||
-p monero-simple-request-rpc \
|
|
||||||
-p monero-address \
|
|
||||||
-p monero-wallet \
|
|
||||||
-p monero-serai-verify-chain
|
|
||||||
|
|||||||
86
Cargo.lock
generated
86
Cargo.lock
generated
@@ -4845,15 +4845,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-address"
|
name = "monero-address"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"hex",
|
|
||||||
"hex-literal",
|
|
||||||
"monero-io",
|
"monero-io",
|
||||||
"monero-primitives",
|
"monero-primitives",
|
||||||
"rand_core",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"thiserror 1.0.64",
|
"thiserror 1.0.64",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -4862,6 +4858,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-borromean"
|
name = "monero-borromean"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
@@ -4874,9 +4871,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-bulletproofs"
|
name = "monero-bulletproofs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"hex-literal",
|
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
"monero-io",
|
"monero-io",
|
||||||
"monero-primitives",
|
"monero-primitives",
|
||||||
@@ -4889,6 +4886,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-clsag"
|
name = "monero-clsag"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
@@ -4909,11 +4907,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-generators"
|
name = "monero-generators"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
"group",
|
"group",
|
||||||
"hex",
|
|
||||||
"monero-io",
|
"monero-io",
|
||||||
"sha3",
|
"sha3",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
@@ -4923,6 +4921,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-io"
|
name = "monero-io"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
@@ -4931,6 +4930,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-mlsag"
|
name = "monero-mlsag"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
@@ -4942,11 +4942,29 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-primitives"
|
name = "monero-oxide"
|
||||||
version = "0.1.0"
|
version = "0.1.4-alpha"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"hex-literal",
|
||||||
|
"monero-borromean",
|
||||||
|
"monero-bulletproofs",
|
||||||
|
"monero-clsag",
|
||||||
|
"monero-generators",
|
||||||
|
"monero-io",
|
||||||
|
"monero-mlsag",
|
||||||
|
"monero-primitives",
|
||||||
|
"std-shims",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "monero-primitives"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"hex",
|
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
"monero-io",
|
"monero-io",
|
||||||
"sha3",
|
"sha3",
|
||||||
@@ -4957,11 +4975,12 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-rpc"
|
name = "monero-rpc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"hex",
|
"hex",
|
||||||
"monero-address",
|
"monero-address",
|
||||||
"monero-serai",
|
"monero-oxide",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
@@ -4969,48 +4988,13 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "monero-serai"
|
|
||||||
version = "0.1.4-alpha"
|
|
||||||
dependencies = [
|
|
||||||
"curve25519-dalek",
|
|
||||||
"hex",
|
|
||||||
"hex-literal",
|
|
||||||
"monero-borromean",
|
|
||||||
"monero-bulletproofs",
|
|
||||||
"monero-clsag",
|
|
||||||
"monero-generators",
|
|
||||||
"monero-io",
|
|
||||||
"monero-mlsag",
|
|
||||||
"monero-primitives",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"std-shims",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "monero-serai-verify-chain"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"curve25519-dalek",
|
|
||||||
"hex",
|
|
||||||
"monero-rpc",
|
|
||||||
"monero-serai",
|
|
||||||
"monero-simple-request-rpc",
|
|
||||||
"rand_core",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-simple-request-rpc"
|
name = "monero-simple-request-rpc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest_auth",
|
"digest_auth",
|
||||||
"hex",
|
"hex",
|
||||||
"monero-address",
|
|
||||||
"monero-rpc",
|
"monero-rpc",
|
||||||
"simple-request",
|
"simple-request",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -5020,6 +5004,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-wallet"
|
name = "monero-wallet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
@@ -5029,18 +5014,14 @@ dependencies = [
|
|||||||
"modular-frost",
|
"modular-frost",
|
||||||
"monero-address",
|
"monero-address",
|
||||||
"monero-clsag",
|
"monero-clsag",
|
||||||
|
"monero-oxide",
|
||||||
"monero-rpc",
|
"monero-rpc",
|
||||||
"monero-serai",
|
|
||||||
"monero-simple-request-rpc",
|
|
||||||
"rand",
|
"rand",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"rand_distr",
|
"rand_distr",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"thiserror 1.0.64",
|
"thiserror 1.0.64",
|
||||||
"tokio",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8346,7 +8327,6 @@ dependencies = [
|
|||||||
"dleq",
|
"dleq",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
"minimal-ed448",
|
"minimal-ed448",
|
||||||
"monero-wallet",
|
|
||||||
"multiexp",
|
"multiexp",
|
||||||
"schnorr-signatures",
|
"schnorr-signatures",
|
||||||
]
|
]
|
||||||
|
|||||||
23
Cargo.toml
23
Cargo.toml
@@ -43,20 +43,6 @@ members = [
|
|||||||
"networks/ethereum",
|
"networks/ethereum",
|
||||||
"networks/ethereum/relayer",
|
"networks/ethereum/relayer",
|
||||||
|
|
||||||
"networks/monero/io",
|
|
||||||
"networks/monero/generators",
|
|
||||||
"networks/monero/primitives",
|
|
||||||
"networks/monero/ringct/mlsag",
|
|
||||||
"networks/monero/ringct/clsag",
|
|
||||||
"networks/monero/ringct/borromean",
|
|
||||||
"networks/monero/ringct/bulletproofs",
|
|
||||||
"networks/monero",
|
|
||||||
"networks/monero/rpc",
|
|
||||||
"networks/monero/rpc/simple-request",
|
|
||||||
"networks/monero/wallet/address",
|
|
||||||
"networks/monero/wallet",
|
|
||||||
"networks/monero/verify-chain",
|
|
||||||
|
|
||||||
"message-queue",
|
"message-queue",
|
||||||
|
|
||||||
"processor/messages",
|
"processor/messages",
|
||||||
@@ -126,13 +112,20 @@ minimal-ed448 = { opt-level = 3 }
|
|||||||
|
|
||||||
multiexp = { opt-level = 3 }
|
multiexp = { opt-level = 3 }
|
||||||
|
|
||||||
monero-serai = { opt-level = 3 }
|
monero-oxide = { opt-level = 3 }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "unwind"
|
panic = "unwind"
|
||||||
overflow-checks = true
|
overflow-checks = true
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
# Dependencies from monero-oxide which originate from within our own tree
|
||||||
|
std-shims = { path = "common/std-shims" }
|
||||||
|
simple-request = { path = "common/request" }
|
||||||
|
dalek-ff-group = { path = "crypto/dalek-ff-group" }
|
||||||
|
flexible-transcript = { path = "crypto/transcript" }
|
||||||
|
modular-frost = { path = "crypto/frost" }
|
||||||
|
|
||||||
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
|
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
|
||||||
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" }
|
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" }
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ unknown-git = "deny"
|
|||||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
allow-git = [
|
allow-git = [
|
||||||
"https://github.com/rust-lang-nursery/lazy-static.rs",
|
"https://github.com/rust-lang-nursery/lazy-static.rs",
|
||||||
|
"https://github.com/monero-oxide/monero-oxide",
|
||||||
"https://github.com/serai-dex/substrate-bip39",
|
"https://github.com/serai-dex/substrate-bip39",
|
||||||
"https://github.com/serai-dex/substrate",
|
"https://github.com/serai-dex/substrate",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
# Ethereum
|
# Ethereum
|
||||||
|
|
||||||
This package contains Ethereum-related functionality, specifically deploying and
|
This package contains Ethereum-related functionality, specifically deploying
|
||||||
interacting with Serai contracts.
|
and interacting with Serai contracts.
|
||||||
|
|
||||||
While `monero-serai` and `bitcoin-serai` are general purpose libraries,
|
While `bitcoin-serai` is a general purpose library, `ethereum-serai` is Serai
|
||||||
`ethereum-serai` is Serai specific. If any of the utilities are generally
|
specific. If any of the utilities are generally desired, please fork and
|
||||||
desired, please fork and maintain your own copy to ensure the desired
|
maintain your own copy to ensure the desired functionality is preserved, or
|
||||||
functionality is preserved, or open an issue to request we make this library
|
open an issue to request we make this library general purpose.
|
||||||
general purpose.
|
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-serai"
|
|
||||||
version = "0.1.4-alpha"
|
|
||||||
description = "A modern Monero transaction library"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
monero-io = { path = "io", version = "0.1", default-features = false }
|
|
||||||
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
|
||||||
monero-primitives = { path = "primitives", version = "0.1", default-features = false }
|
|
||||||
monero-mlsag = { path = "ringct/mlsag", version = "0.1", default-features = false }
|
|
||||||
monero-clsag = { path = "ringct/clsag", version = "0.1", default-features = false }
|
|
||||||
monero-borromean = { path = "ringct/borromean", version = "0.1", default-features = false }
|
|
||||||
monero-bulletproofs = { path = "ringct/bulletproofs", version = "0.1", default-features = false }
|
|
||||||
|
|
||||||
hex-literal = "0.4"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
serde = { version = "1", default-features = false, features = ["std", "derive"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
|
|
||||||
"monero-io/std",
|
|
||||||
"monero-generators/std",
|
|
||||||
"monero-primitives/std",
|
|
||||||
"monero-mlsag/std",
|
|
||||||
"monero-clsag/std",
|
|
||||||
"monero-borromean/std",
|
|
||||||
"monero-bulletproofs/std",
|
|
||||||
]
|
|
||||||
|
|
||||||
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-bulletproofs/compile-time-generators"]
|
|
||||||
default = ["std", "compile-time-generators"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# monero-serai
|
|
||||||
|
|
||||||
A modern Monero transaction library. It provides a modern, Rust-friendly view of
|
|
||||||
the Monero protocol.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
Recommended usage of the library is with `overflow-checks = true`, even for
|
|
||||||
release builds.
|
|
||||||
|
|
||||||
### Wallet Functionality
|
|
||||||
|
|
||||||
monero-serai originally included wallet functionality. That has been moved to
|
|
||||||
monero-wallet.
|
|
||||||
|
|
||||||
### Purpose and Support
|
|
||||||
|
|
||||||
monero-serai was written for Serai, a decentralized exchange aiming to support
|
|
||||||
Monero. Despite this, monero-serai is intended to be a widely usable library,
|
|
||||||
accurate to Monero. monero-serai guarantees the functionality needed for Serai,
|
|
||||||
yet does not include any functionality specific to Serai.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
- `compile-time-generators` (on by default): Derives the generators at
|
|
||||||
compile-time so they don't need to be derived at runtime. This is recommended
|
|
||||||
if program size doesn't need to be kept minimal.
|
|
||||||
- `multisig`: Enables the `multisig` feature for all dependencies.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-generators"
|
|
||||||
version = "0.4.0"
|
|
||||||
description = "Monero's hash to point function and generators"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/generators"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
subtle = { version = "^2.4", default-features = false }
|
|
||||||
|
|
||||||
sha3 = { version = "0.10", default-features = false }
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
monero-io = { path = "../io", version = "0.1", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = "0.4"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"subtle/std",
|
|
||||||
|
|
||||||
"sha3/std",
|
|
||||||
|
|
||||||
"group/alloc",
|
|
||||||
"dalek-ff-group/std",
|
|
||||||
|
|
||||||
"monero-io/std"
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Monero Generators
|
|
||||||
|
|
||||||
Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
|
||||||
An implementation of Monero's `hash_to_ec` is included, as needed to generate
|
|
||||||
the generators.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
use subtle::ConditionallySelectable;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
|
||||||
|
|
||||||
use group::ff::{Field, PrimeField};
|
|
||||||
use dalek_ff_group::FieldElement;
|
|
||||||
|
|
||||||
use monero_io::decompress_point;
|
|
||||||
|
|
||||||
use crate::keccak256;
|
|
||||||
|
|
||||||
/// Monero's `hash_to_ec` function.
|
|
||||||
pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let A = FieldElement::from(486662u64);
|
|
||||||
|
|
||||||
let v = FieldElement::from_square(keccak256(&bytes)).double();
|
|
||||||
let w = v + FieldElement::ONE;
|
|
||||||
let x = w.square() + (-A.square() * v);
|
|
||||||
|
|
||||||
// This isn't the complete X, yet its initial value
|
|
||||||
// We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X
|
|
||||||
// While inefficient, it solves API boundaries and reduces the amount of work done here
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let X = {
|
|
||||||
let u = w;
|
|
||||||
let v = x;
|
|
||||||
let v3 = v * v * v;
|
|
||||||
let uv3 = u * v3;
|
|
||||||
let v7 = v3 * v3 * v;
|
|
||||||
let uv7 = u * v7;
|
|
||||||
uv3 *
|
|
||||||
uv7.pow(
|
|
||||||
(-FieldElement::from(5u8)) *
|
|
||||||
FieldElement::from(8u8).invert().expect("eight was coprime with the prime 2^{255}-19"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let x = X.square() * x;
|
|
||||||
|
|
||||||
let y = w - x;
|
|
||||||
let non_zero_0 = !y.is_zero();
|
|
||||||
let y_if_non_zero_0 = w + x;
|
|
||||||
let sign = non_zero_0 & (!y_if_non_zero_0.is_zero());
|
|
||||||
|
|
||||||
let mut z = -A;
|
|
||||||
z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let Z = z + w;
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut Y = z - w;
|
|
||||||
|
|
||||||
/*
|
|
||||||
If sign, `z = -486662`, else, `z = -486662 * v`
|
|
||||||
`w = v + 1`
|
|
||||||
|
|
||||||
We need `z + w \ne 0`, which would require `z \cong -w \mod 2^{255}-19`. This requires:
|
|
||||||
- If `sign`, `v \mod 2^{255}-19 \ne 486661`.
|
|
||||||
- If `!sign`, `(v + 1) \mod 2^{255}-19 \ne (v * 486662) \mod 2^{255}-19` which is equivalent to
|
|
||||||
`(v * 486661) \mod 2^{255}-19 \ne 1`.
|
|
||||||
|
|
||||||
In summary, if `sign`, `v` must not `486661`, and if `!sign`, `v` must not be the
|
|
||||||
multiplicative inverse of `486661`. Since `v` is the output of a hash function, this should
|
|
||||||
have negligible probability. Additionally, since the definition of `sign` is dependent on `v`,
|
|
||||||
it may be truly impossible to reach.
|
|
||||||
*/
|
|
||||||
Y *= Z.invert().expect("if sign, v was 486661. if !sign, v was 486661^{-1}");
|
|
||||||
let mut bytes = Y.to_repr();
|
|
||||||
bytes[31] |= sign.unwrap_u8() << 7;
|
|
||||||
|
|
||||||
decompress_point(bytes).expect("point from hash-to-curve wasn't on-curve").mul_by_cofactor()
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use std_shims::{sync::LazyLock, vec::Vec};
|
|
||||||
|
|
||||||
use sha3::{Digest, Keccak256};
|
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_io::{write_varint, decompress_point};
|
|
||||||
|
|
||||||
mod hash_to_point;
|
|
||||||
pub use hash_to_point::hash_to_point;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
fn keccak256(data: &[u8]) -> [u8; 32] {
|
|
||||||
Keccak256::digest(data).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Monero's `H` generator.
|
|
||||||
///
|
|
||||||
/// Contrary to convention (`G` for values, `H` for randomness), `H` is used by Monero for amounts
|
|
||||||
/// within Pedersen commitments.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub static H: LazyLock<EdwardsPoint> = LazyLock::new(|| {
|
|
||||||
decompress_point(keccak256(&ED25519_BASEPOINT_POINT.compress().to_bytes()))
|
|
||||||
.expect("known on-curve point wasn't on-curve")
|
|
||||||
.mul_by_cofactor()
|
|
||||||
});
|
|
||||||
|
|
||||||
static H_POW_2_CELL: LazyLock<[EdwardsPoint; 64]> = LazyLock::new(|| {
|
|
||||||
let mut res = [*H; 64];
|
|
||||||
for i in 1 .. 64 {
|
|
||||||
res[i] = res[i - 1] + res[i - 1];
|
|
||||||
}
|
|
||||||
res
|
|
||||||
});
|
|
||||||
/// Monero's `H` generator, multiplied by 2**i for i in 1 ..= 64.
|
|
||||||
///
|
|
||||||
/// This table is useful when working with amounts, which are u64s.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn H_pow_2() -> &'static [EdwardsPoint; 64] {
|
|
||||||
&H_POW_2_CELL
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The maximum amount of commitments provable for within a single range proof.
|
|
||||||
pub const MAX_COMMITMENTS: usize = 16;
|
|
||||||
/// The amount of bits a value within a commitment may use.
|
|
||||||
pub const COMMITMENT_BITS: usize = 64;
|
|
||||||
|
|
||||||
/// Container struct for Bulletproofs(+) generators.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub struct Generators {
|
|
||||||
/// The G (bold) vector of generators.
|
|
||||||
pub G: Vec<EdwardsPoint>,
|
|
||||||
/// The H (bold) vector of generators.
|
|
||||||
pub H: Vec<EdwardsPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate generators as needed for Bulletproofs(+), as Monero does.
|
|
||||||
///
|
|
||||||
/// Consumers should not call this function ad-hoc, yet call it within a build script or use a
|
|
||||||
/// once-initialized static.
|
|
||||||
pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators {
|
|
||||||
// The maximum amount of bits used within a single range proof.
|
|
||||||
const MAX_MN: usize = MAX_COMMITMENTS * COMMITMENT_BITS;
|
|
||||||
|
|
||||||
let mut preimage = H.compress().to_bytes().to_vec();
|
|
||||||
preimage.extend(dst);
|
|
||||||
|
|
||||||
let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) };
|
|
||||||
for i in 0 .. MAX_MN {
|
|
||||||
// We generate a pair of generators per iteration
|
|
||||||
let i = 2 * i;
|
|
||||||
|
|
||||||
let mut even = preimage.clone();
|
|
||||||
write_varint(&i, &mut even).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res.H.push(hash_to_point(keccak256(&even)));
|
|
||||||
|
|
||||||
let mut odd = preimage.clone();
|
|
||||||
write_varint(&(i + 1), &mut odd).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res.G.push(hash_to_point(keccak256(&odd)));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
use crate::{decompress_point, hash_to_point};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vectors() {
|
|
||||||
// tests.txt file copied from monero repo
|
|
||||||
// https://github.com/monero-project/monero/
|
|
||||||
// blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/tests/crypto/tests.txt
|
|
||||||
let reader = include_str!("./tests.txt");
|
|
||||||
|
|
||||||
for line in reader.lines() {
|
|
||||||
let mut words = line.split_whitespace();
|
|
||||||
let command = words.next().unwrap();
|
|
||||||
|
|
||||||
match command {
|
|
||||||
"check_key" => {
|
|
||||||
let key = words.next().unwrap();
|
|
||||||
let expected = match words.next().unwrap() {
|
|
||||||
"true" => true,
|
|
||||||
"false" => false,
|
|
||||||
_ => unreachable!("invalid result"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let actual = decompress_point(hex::decode(key).unwrap().try_into().unwrap());
|
|
||||||
assert_eq!(actual.is_some(), expected);
|
|
||||||
}
|
|
||||||
"hash_to_ec" => {
|
|
||||||
let bytes = words.next().unwrap();
|
|
||||||
let expected = words.next().unwrap();
|
|
||||||
|
|
||||||
let actual = hash_to_point(hex::decode(bytes).unwrap().try_into().unwrap());
|
|
||||||
assert_eq!(hex::encode(actual.compress().to_bytes()), expected);
|
|
||||||
}
|
|
||||||
_ => unreachable!("unknown command"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,628 +0,0 @@
|
|||||||
check_key c2cb3cf3840aa9893e00ec77093d3d44dba7da840b51c48462072d58d8efd183 false
|
|
||||||
check_key bd85a61bae0c101d826cbed54b1290f941d26e70607a07fc6f0ad611eb8f70a6 true
|
|
||||||
check_key 328f81cad4eba24ab2bad7c0e56b1e2e7346e625bcb06ae649aef3ffa0b8bef3 false
|
|
||||||
check_key 6016a5463b9e5a58c3410d3f892b76278883473c3f0b69459172d3de49e85abe true
|
|
||||||
check_key 4c71282b2add07cdc6898a2622553f1ca4eb851e5cb121181628be5f3814c5b1 false
|
|
||||||
check_key 69393c25c3b50e177f81f20f852dd604e768eb30052e23108b3cfa1a73f2736e true
|
|
||||||
check_key 3d5a89b676cb84c2be3428d20a660dc6a37cae13912e127888a5132e8bac2163 true
|
|
||||||
check_key 78cd665deb28cebc6208f307734c56fccdf5fa7e2933fadfcdd2b6246e9ae95c false
|
|
||||||
check_key e03b2414e260580f86ee294cd4c636a5b153e617f704e81dad248fbf715b2ee4 true
|
|
||||||
check_key 28c3503ce82d7cdc8e0d96c4553bcf0352bbcfc73925495dbe541e7e1df105fc false
|
|
||||||
check_key 06855c3c3e0d03fec354059bda319b39916bdc10b6581e3f41b335ee7b014fd5 false
|
|
||||||
check_key 556381485df0d7d5a268ab5ecfb2984b060acc63471183fcf538bf273b0c0cb5 true
|
|
||||||
check_key c7f76d82ac64b1e7fdc32761ff00d6f0f7ada4cf223aa5a11187e3a02e1d5319 true
|
|
||||||
check_key cfa85d8bdb6f633fcf031adee3a299ac42eeb6bd707744049f652f6322f5aa47 true
|
|
||||||
check_key 91e9b63ced2b08979fee713365464cc3417c4f238f9bdd3396efbb3c58e195ee true
|
|
||||||
check_key 7b56e76fe94bd30b3b2f2c4ba5fe4c504821753a8965eb1cbcf8896e2d6aba19 true
|
|
||||||
check_key 7338df494bc416cf5edcc02069e067f39cb269ce67bd9faba956021ce3b3de3a false
|
|
||||||
check_key f9a1f27b1618342a558379f4815fa5039a8fe9d98a09f45c1af857ba99231dc1 false
|
|
||||||
check_key b2a1f37718180d4448a7fcb5f788048b1a7132dde1cfd25f0b9b01776a21c687 true
|
|
||||||
check_key 0d3a0f9443a8b24510ad1e76a8117cca03bce416edfe35e3c2a2c2712454f8dc false
|
|
||||||
check_key d8d3d806a76f120c4027dc9c9d741ad32e06861b9cfbc4ce39289c04e251bb3c false
|
|
||||||
check_key 1e9e3ba7bc536cd113606842835d1f05b4b9e65875742f3a35bfb2d63164b5d5 true
|
|
||||||
check_key 5c52d0087997a2cdf1d01ed0560d94b4bfd328cb741cb9a8d46ff50374b35a57 true
|
|
||||||
check_key bb669d4d7ffc4b91a14defedcdbd96b330108b01adc63aa685e2165284c0033b false
|
|
||||||
check_key d2709ae751a0a6fd796c98456fa95a7b64b75a3434f1caa3496eeaf5c14109b4 true
|
|
||||||
check_key e0c238cba781684e655b10a7d4af04ab7ff2e7022182d7ed2279d6adf36b3e7a false
|
|
||||||
check_key 34ebb4bf871572cee5c6935716fab8c8ec28feef4f039763d8f039b84a50bf4c false
|
|
||||||
check_key 4730d4f38ec3f3b83e32e6335d2506df4ee39858848842c5a0184417fcc639e4 true
|
|
||||||
check_key d42cf7fdf5e17e0a8a7f88505a2b7a3d297113bd93d3c20fa87e11509ec905a2 true
|
|
||||||
check_key b757c95059cefabb0080d3a8ebca82e46efecfd29881be3121857f9d915e388c false
|
|
||||||
check_key bbe777aaf04d02b96c0632f4b1c6f35f1c7bcbc5f22af192f92c077709a2b50b false
|
|
||||||
check_key 73518522aabd28566f858c33fccb34b7a4de0e283f6f783f625604ee647afad9 true
|
|
||||||
check_key f230622c4a8f6e516590466bd10f86b64fbef61695f6a054d37604e0b024d5af false
|
|
||||||
check_key bc6b9a8379fd6c369f7c3bd9ddce58db6b78f27a41d798bb865c3920824d0943 false
|
|
||||||
check_key 45a4f87c25898cd6be105fa1602b85c4d862782adaac8b85c996c4a2bcd8af47 true
|
|
||||||
check_key eb4ad3561d21c4311affbd7cc2c7ff5fd509f72f88ba67dc097a75c31fdbd990 false
|
|
||||||
check_key 2f34f4630c09a23b7ecc19f02b4190a26df69e07e13de8069ae5ff80d23762fc true
|
|
||||||
check_key 2ea4e4fb5085eb5c8adee0d5ab7d35c67d74d343bd816cd13924536cffc2527c true
|
|
||||||
check_key 5d35467ee6705a0d35818aa9ae94e4603c3e5500bfc4cf4c4f77a7160a597aa6 true
|
|
||||||
check_key 8ff42bc76796e20c99b6e879369bd4b46a256db1366416291de9166e39d5a093 true
|
|
||||||
check_key 0262ba718850df6c621e8a24cd9e4831c047e38818a89e15c7a06a489a4558e1 false
|
|
||||||
check_key 58b29b2ba238b534b08fb46f05f430e61cb77dc251b0bb50afec1b6061fd9247 false
|
|
||||||
check_key 153170e3dc2b0e1b368fc0d0e31053e872f094cdace9a2846367f0d9245a109b false
|
|
||||||
check_key 40419d309d07522d493bb047ca9b5fb6c401aae226eefae6fd395f5bb9114200 true
|
|
||||||
check_key 713068818d256ef69c78cd6082492013fbd48de3c9e7e076415dd0a692994504 true
|
|
||||||
check_key a7218ee08e50781b0c87312d5e0031467e863c10081668e3792d96cbcee4e474 true
|
|
||||||
check_key 356ce516b00e674ef1729c75b0a68090e7265cef675bbf32bf809495b67e9342 false
|
|
||||||
check_key 52a5c053293675e3efd2c585047002ea6d77931cbf38f541b9070d319dc0d237 false
|
|
||||||
check_key 77c0080bf157e069b18c4c604cc9505c5ec6f0f9930e087592d70507ca1b5534 false
|
|
||||||
check_key e733bc41f880a4cfb1ca6f397916504130807289cacfca10b15f5b8d058ed1bf false
|
|
||||||
check_key c4f1d3c884908a574ecea8be10e02277de35ef84a1d10f105f2be996f285161f true
|
|
||||||
check_key aed677f7f69e146aa0863606ac580fc0bbdc22a88c4b4386abaa4bdfff66bcc9 false
|
|
||||||
check_key 6ad0edf59769599af8caa986f502afc67aecbebb8107aaf5e7d3ae51d5cf8dd8 false
|
|
||||||
check_key 64a0a70e99be1f775c222ee9cd6f1bee6f632cb9417899af398ff9aff70661c6 true
|
|
||||||
check_key c63afaa03bb5c4ed7bc77aac175dbfb73f904440b2e3056a65850ac1bd261332 false
|
|
||||||
check_key a4e89cd2471c26951513b1cfbdcf053a86575e095af52495276aa56ede8ce344 false
|
|
||||||
check_key 2ce935d97f7c3ddb973de685d20f58ee39938fe557216328045ec2b83f3132be true
|
|
||||||
check_key 3e3d38b1fca93c1559ac030d586616354c668aa76245a09e3fa6de55ac730973 true
|
|
||||||
check_key 8b81b9681f76a4254007fd07ed1ded25fc675973ccb23afd06074805194733a4 false
|
|
||||||
check_key 26d1c15dfc371489439e29bcef2afcf7ed01fac24960fdc2e7c20847a8067588 true
|
|
||||||
check_key 85c1199b5a4591fc4cc36d23660648c1b9cfbb0e9c47199fa3eea33299a3dcec false
|
|
||||||
check_key 60830ba5449c1f04ac54675dfc7cac7510106c4b7549852551f8fe65971123e2 false
|
|
||||||
check_key 3e43c28c024597b3b836e4bc16905047cbf6e841b80e0b8cd6a325049070c2a5 false
|
|
||||||
check_key 474792c16a0032343a6f28f4cb564747c3b1ea0b6a6b9a42f7c71d7cc3dd3b44 true
|
|
||||||
check_key c8ec5e67cb5786673085191881950a3ca20dde88f46851b01dd91c695cfbad16 true
|
|
||||||
check_key 861c4b24b24a87b8559e0bb665f84dcc506c147a909f335ae4573b92299f042f false
|
|
||||||
check_key 2c9e0fe3e4983d79f86c8c36928528f1bc90d94352ce427032cdef6906d84d0b true
|
|
||||||
check_key 9293742822c2dff63fdc1bf6645c864fd527cea2ddba6d4f3048d202fc340c9a true
|
|
||||||
check_key 3956422ad380ef19cb9fe360ef09cc7aaec7163eea4114392a7a0b2e2671914e true
|
|
||||||
check_key 5ae8e72cadda85e525922fec11bd53a261cf26ee230fe85a1187f831b1b2c258 false
|
|
||||||
check_key 973feca43a0baf450c30ace5dc19015e19400f0898316e28d9f3c631da31f99a true
|
|
||||||
check_key dd946c91a2077f45c5c16939e53859d9beabaf065e7b1b993d5e5cd385f8716e true
|
|
||||||
check_key b3928f2d67e47f6bd6da81f72e64908d8ff391af5689f0202c4c6fec7666ffe8 true
|
|
||||||
check_key 313382e82083697d7f9d256c3b3800b099b56c3ef33cacdccbd40a65622e25fc false
|
|
||||||
check_key 7d65380c12144802d39ed9306eed79fe165854273700437c0b4b50559800c058 true
|
|
||||||
check_key 4db5c20a49422fd27739c9ca80e2271a8a125dfcead22cb8f035d0e1b7b163be true
|
|
||||||
check_key dd76a9f565ef0e44d1531349ec4c5f7c3c387c2f5823e693b4952f4b0b70808c true
|
|
||||||
check_key 66430bf628eae23918c3ed17b42138db1f98c24819e55fc4a07452d0c85603eb true
|
|
||||||
check_key 9f0b677830c3f089c27daf724bb10be848537f8285de83ab0292d35afb617f77 false
|
|
||||||
check_key cbf98287391fb00b1e68ad64e9fb10198025864c099b8b9334d840457e673874 true
|
|
||||||
check_key a42552e9446e49a83aed9e3370506671216b2d1471392293b8fc2b81c81a73ee false
|
|
||||||
check_key fb3de55ac81a923d506a514602d65d004ec9d13e8b47e82d73af06da73006673 false
|
|
||||||
check_key e17abb78e58a4b72ff4ad7387b290f2811be880b394b8bcaae7748ac09930169 false
|
|
||||||
check_key 9ffbda7ace69753761cdb5eb01f75433efa5cdb6a4f1b664874182c6a95adcba true
|
|
||||||
check_key 507123c979179ea0a3f7f67fb485f71c8636ec4ec70aa47b92f3c707e7541a54 false
|
|
||||||
check_key f1d0b156571994ef578c61cb6545d34f834eb30e4357539a5633c862d4dffa91 false
|
|
||||||
check_key 3de62311ec14f9ee95828c190b2dc3f03059d6119e8dfccb7323efc640e07c75 false
|
|
||||||
check_key 5e50bb48bc9f6dd11d52c1f0d10d8ae5674d7a4af89cbbce178dafc8a562e5fe false
|
|
||||||
check_key 20b2c16497be101995391ceefb979814b0ea76f1ed5b6987985bcdcd17b36a81 false
|
|
||||||
check_key d63bff73b914ce791c840e99bfae0d47afdb99c2375e33c8f149d0df03d97873 false
|
|
||||||
check_key 3f24b3d94b5ddd244e4c4e67a6d9f533f0396ca30454aa0ca799f21328b81d47 true
|
|
||||||
check_key 6a44c016f09225a6d2e830290719d33eb29b53b553eea7737ed3a6e297b2e7d2 true
|
|
||||||
check_key ff0f34df0c76c207b8340be2009db72f730c69c2bbfeea2013105eaccf1d1f8e true
|
|
||||||
check_key 4baf559869fe4e915e219c3c8d9a2330fc91e542a5a2a7311d4d59fee996f807 true
|
|
||||||
check_key 1632207dfef26e97d13b0d0035ea9468fc5a8a89b0990fce77bb143c9d7f3b67 true
|
|
||||||
check_key fcb3dee3993d1a47630f29410903dd03706bd5e81c5802e6f1b9095cbdb404d3 true
|
|
||||||
check_key fb527092b9809e3d27d7588c7ef89915a769b99c1e03e7f72bbead9ed837daae false
|
|
||||||
check_key 902b118d27d40ab9cbd55edd375801ce302cdb59e09c8659a3ea1401918d8bba false
|
|
||||||
check_key 4d6fbf25ca51e263a700f1abf84f758dde3d11b632e908b3093d64fe2e70ea0a true
|
|
||||||
check_key f4c3211ec70affc1c9a94a6589460ee8360dad5f8c679152f16994038532e3fc true
|
|
||||||
check_key c2b3d73ac14956d7fdf12fa92235af1bb09e1566a6a6ffd0025682c750abdd69 false
|
|
||||||
check_key b7e68c12207d2e2104fb2ca224829b6fccc1c0e2154e8a931e3c837a945f4430 false
|
|
||||||
check_key 56ca0ca227708f1099bda1463db9559541c8c11ffad7b3d95c717471f25a01bf true
|
|
||||||
check_key 3eef3a46833e4d851671182a682e344e36bea7211a001f3b8af1093a9c83f1b2 true
|
|
||||||
check_key bd1f4a4f26cab7c1cbc0e17049b90854d6d28d2d55181e1b5f7a8045fcdfa06e true
|
|
||||||
check_key 8537b01c87e7c184d9555e8d93363dcd9b60a8acc94cd3e41eb7525fd3e1d35a false
|
|
||||||
check_key 68ace49179d549bad391d98ab2cc8afee65f98ce14955c3c1b16e850fabec231 true
|
|
||||||
check_key f9922f8a660e7c3e4f3735a817d18b72f59166a0be2d99795f953cf233a27e24 true
|
|
||||||
check_key 036b6be3da26e80508d5a5a6a5999a1fe0db1ac4e9ade8f1ea2eaf2ea9b1a70e true
|
|
||||||
check_key 5e595e886ce16b5ea31f53bcb619f16c8437276618c595739fece6339731feb0 false
|
|
||||||
check_key 4ee2cebae3476ed2eeb7efef9d20958538b3642f938403302682a04115c0f8ed false
|
|
||||||
check_key 519eedbd0da8676063ce7d5a605b3fc27afeecded857afa24b894ad248c87b5d false
|
|
||||||
check_key ce2b627c0accf4a3105796680c37792b30c6337d2d4fea11678282455ff82ff7 false
|
|
||||||
check_key aa26ed99071a8416215e8e7ded784aa7c2b303aab67e66f7539905d7e922eb4d false
|
|
||||||
check_key 435ae49c9ca26758aa103bdcca8d51393b1906fe27a61c5245361e554f335ec2 true
|
|
||||||
check_key 42568af395bd30024f6ccc95205c0e11a6ad1a7ee100f0ec46fcdf0af88e91fb false
|
|
||||||
check_key 0b4a78d1fde56181445f04ca4780f0725daa9c375b496fab6c037d6b2c2275db true
|
|
||||||
check_key 2f82d2a3c8ce801e1ad334f9e074a4fbf76ffac4080a7331dc1359c2b4f674a4 false
|
|
||||||
check_key 24297d8832d733ed052dd102d4c40e813f702006f325644ccf0cb2c31f77953f false
|
|
||||||
check_key 5231a53f6bea7c75b273bde4a9f673044ed87796f20e0909978f29d98fc8d4f0 true
|
|
||||||
check_key 94b5affcf78be5cf62765c32a0794bc06b4900e8a47ddba0e166ec20cec05935 true
|
|
||||||
check_key c14b4d846ea52ffbbb36aa62f059453af3cfae306280dada185d2d385ef8f317 true
|
|
||||||
check_key cceb34fddf01a6182deb79c6000a998742d4800d23d1d8472e3f43cd61f94508 true
|
|
||||||
check_key 1faffa33407fba1634d4136cf9447896776c16293b033c6794f06774b514744c true
|
|
||||||
check_key faaac98f644a2b77fb09ba0ebf5fcddf3ff55f6604c0e9e77f0278063e25113a true
|
|
||||||
check_key 09e8525b00bea395978279ca979247a76f38f86dce4465eb76c140a7f904c109 true
|
|
||||||
check_key 2d797fc725e7fb6d3b412694e7386040effe4823cdf01f6ec7edea4bc0e77e20 false
|
|
||||||
check_key bbb74dabee651a65f46bca472df6a8a749cc4ba5ca35078df5f6d27a772f922a false
|
|
||||||
check_key 77513ca00f3866607c3eff5c2c011beffa775c0022c5a4e7de1120a27e6687fd true
|
|
||||||
check_key 10064c14ace2a998fc2843eeeb62884fe3f7ab331ca70613d6a978f44d9868eb false
|
|
||||||
check_key 026ae84beb5e54c62629a7b63702e85044e38cadfc9a1fcabee6099ba185005c false
|
|
||||||
check_key aef91536292b7ba34a3e787fb019523c2fa7a0d56fca069cc82ccb6b02a45b14 false
|
|
||||||
check_key 147bb1a82c623c722540feaad82b7adf4b85c6ec0cbcef3ca52906f3e85617ac true
|
|
||||||
check_key fc9fb281a0847d58dc9340ef35ef02f7d20671142f12bdd1bfb324ab61d03911 false
|
|
||||||
check_key b739801b9455ac617ca4a7190e2806669f638d4b2f9288171afb55e1542c8d71 false
|
|
||||||
check_key 494cc1e2ee997eb1eb051f83c4c89968116714ddf74e460d4fa1c6e7c72e3eb3 true
|
|
||||||
check_key ed2fbdf2b727ed9284db90ec900a942224787a880bc41d95c4bc4cf136260fd7 true
|
|
||||||
check_key 02843d3e6fc6835ad03983670a592361a26948eb3e31648d572416a944d4909e true
|
|
||||||
check_key c14fea556a7e1b6b6c3d4e2e38a4e7e95d834220ff0140d3f7f561a34e460801 true
|
|
||||||
check_key 5f8f82a35452d0b0d09ffb40a1154641916c31e161ad1a6ab8cfddc2004efdf6 false
|
|
||||||
check_key 7b93d72429fab07b49956007eba335bb8c5629fbf9e7a601eaa030f196934a56 true
|
|
||||||
check_key 6a63ed96d2e46c2874beaf82344065d94b1e5c04406997f94caf4ccd97cfbab9 false
|
|
||||||
check_key c915f409e1e0f776d1f440aa6969cfec97559ef864b07d8c0d7c1163871b4603 true
|
|
||||||
check_key d06bc33630fc94303c2c369481308f805f5ce53c40141160aa4a1f072967617e false
|
|
||||||
check_key 1aafb14ca15043c2589bcd32c7c5f29479216a1980e127e9536729faf1c40266 true
|
|
||||||
check_key 58c115624a20f4b0c152ccd048c54a28a938556863ab8521b154d3165d3649cd false
|
|
||||||
check_key 9001ba086e8aa8a67e128f36d700cc641071556306db7ec9b8ac12a6256b27b7 false
|
|
||||||
check_key 898c468541634fb0def11f82c781341fce0def7b15695af4e642e397218c730c true
|
|
||||||
check_key 47ea6539e65b7b611b0e1ae9ee170adf7c31581ca9f78796d8ebbcc5cd74b712 false
|
|
||||||
check_key 0c60952a64eeac446652f5d3c136fd36966cf66310c15ee6ab2ecbf981461257 false
|
|
||||||
check_key 682264c4686dc7736b6e46bdc8ab231239bc5dac3f5cb9681a1e97a527945e8e true
|
|
||||||
check_key 276006845ca0ea4238b231434e20ad8b8b2a36876effbe1d1e3ffb1f14973397 true
|
|
||||||
check_key eecd3a49e55e32446f86c045dce123ef6fe2e5c57db1d850644b3c56ec689fce true
|
|
||||||
check_key a4dced63589118db3d5aebf6b5670e71250f07485ca4bb6dddf9cce3e4c227a1 false
|
|
||||||
check_key b8ade608ba43d55db7ab481da88b74a9be513fca651c03e04d30cc79f50e0276 false
|
|
||||||
check_key 0d91de88d007a03fe782f904808b036ff63dec6b73ce080c55231afd4ed261c3 true
|
|
||||||
check_key 87c59becb52dd16501edadbb0e06b0406d69541c4d46115351e79951a8dd9c28 true
|
|
||||||
check_key 9aee723be2265171fe10a86d1d3e9cf5a4e46178e859db83f86d1c6db104a247 false
|
|
||||||
check_key 509d34ae5bf56db011845b8cdf0cc7729ed602fce765e9564cb433b4d4421a43 false
|
|
||||||
check_key 06e766d9a6640558767c2aab29f73199130bfdc07fd858a73e6ae8e7b7ba23ba false
|
|
||||||
check_key 801c4fe5ab3e7cf13f7aa2ca3bc57cc8eba587d21f8bc4cd40b1e98db7aec8d9 false
|
|
||||||
check_key d85ad63aeb7d2faa22e5c9b87cd27f45b01e6d0fdc4c3ddf105584ac0a021465 false
|
|
||||||
check_key a7ca13051eb2baeb5befa5e236e482e0bb71803ad06a6eae3ae48742393329d2 true
|
|
||||||
check_key 5a9ba3ec20f116173d933bf5cf35c320ed3751432f3ab453e4a6c51c1d243257 false
|
|
||||||
check_key a4091add8a6710c03285a422d6e67863a48b818f61c62e989b1e9b2ace240a87 false
|
|
||||||
check_key bdee0c6442e6808f25bb18e21b19032cf93a55a5f5c6426fba2227a41c748684 true
|
|
||||||
check_key d4aeb6cdad9667ec3b65c7fbc5bfd1b82bba1939c6bb448a86e40aec42be5f25 false
|
|
||||||
check_key 73525b30a77f1212f7e339ec11f48c453e476f3669e6e70bebabc2fe9e37c160 true
|
|
||||||
check_key 45501f2dc4d0a3131f9e0fe37a51c14869ab610abd8bf0158111617924953629 false
|
|
||||||
check_key 07d0e4c592aa3676adf81cca31a95d50c8c269d995a78cde27b2a9a7a93083a6 false
|
|
||||||
check_key a1797d6178c18add443d22fdbf45ca5e49ead2f78b70bdf1500f570ee90adca5 true
|
|
||||||
check_key 0961e82e6e7855d7b7bf96777e14ae729f91c5bbd20f805bd7daac5ccbec4bab false
|
|
||||||
check_key 57f5ba0ad36e997a4fb585cd2fc81b9cc5418db702c4d1e366639bb432d37c73 true
|
|
||||||
check_key 82b005be61580856841e042ee8be74ae4ca66bb6733478e81ca1e56213de5c05 false
|
|
||||||
check_key d7733dcae1874c93e9a2bd46385f720801f913744d60479930dad7d56c767cdc false
|
|
||||||
check_key b8b8b698609ac3f1bd8f4965151b43b362e6c5e3d1c1feae312c1d43976d59ab true
|
|
||||||
check_key 4bba7815a9a1b86a5b80b17ac0b514e2faa7a24024f269b330e5b7032ae8c04e true
|
|
||||||
check_key 0f70da8f8266b58acda259935ef1a947c923f8698622c5503520ff31162e877b false
|
|
||||||
check_key 233eaa3db80f314c6c895d1328a658a9175158fa2483ed216670c288a04b27bc false
|
|
||||||
check_key a889f124fabfd7a1e2d176f485be0cbd8b3eeaafeee4f40e99e2a56befb665be true
|
|
||||||
check_key 2b7b8abc198b11cf7efa21bc63ec436f790fe1f9b8c044440f183ab291af61d6 true
|
|
||||||
check_key 2491804714f7938cf501fb2adf07597b4899b919cabbaab49518b8f8767fdc6a true
|
|
||||||
check_key 52744a54fcb00dc930a5d7c2bc866cbfc1e75dd38b38021fd792bb0ca9f43164 true
|
|
||||||
check_key e42cbf70b81ba318419104dffbb0cdc3b7e7d4698e422206b753a4e2e6fc69bb false
|
|
||||||
check_key 2faff73e4fed62965f3dbf2e6446b5fea0364666cc8c9450b6ed63bbb6f5f0e7 true
|
|
||||||
check_key 8b963928d75be661c3c18ddd4f4d1f37ebc095ce1edc13fe8b23784c8f416dfd false
|
|
||||||
check_key b1162f952808434e4d2562ffda98bd311613d655d8cf85dc86e0a6c59f7158bc true
|
|
||||||
check_key 5a69adcd9e4f5b0020467e968d85877cb3aa04fa86088d4499b57ca65a665836 true
|
|
||||||
check_key 61ab47da432c829d0bc9d4fdb59520b135428eec665ad509678188b81c7adf49 false
|
|
||||||
check_key 154bb547f22f65a87c0c3f56294f5791d04a3c14c8125d256aeed8ec54c4a06e true
|
|
||||||
check_key 0a78197861c30fd3547b5f2eabd96d3ac22ac0632f03b7afd9d5d2bfc2db352f true
|
|
||||||
check_key 8bdeadcca1f1f8a4a67b01ed2f10ef31aba7b034e8d1df3a69fe9aebf32454e0 false
|
|
||||||
check_key f4b17dfca559be7d5cea500ac01e834624fed9befae3af746b39073d5f63190d true
|
|
||||||
check_key 622c52821e16ddc63b58f3ec2b959fe8c6ea6b1a596d9a58fd81178963f41c01 true
|
|
||||||
check_key 07bedd5d55c937ef5e23a56c6e58f31adb91224d985285d7fef39ede3a9efb17 false
|
|
||||||
check_key 5179bf3b7458648e57dc20f003c6bbfd55e8cd7c0a6e90df6ef8e8183b46f99d true
|
|
||||||
check_key 683c80c3f304f10fdd53a84813b5c25b1627ebd14eb29b258b41cd14396ef41f true
|
|
||||||
check_key c266244ed597c438170875fe7874f81258a830105ca1108131e6b8fea95eb8ba true
|
|
||||||
check_key 0c1cdc693df29c2d1e66b2ce3747e34a30287d5eb6c302495634ec856593fe8e true
|
|
||||||
check_key 28950f508f6a0d4c20ab5e4d55b80565a6a539092e72b7eb0ed9fa5017ecef88 false
|
|
||||||
check_key 8328a2a5fcfc4433b1c283539a8943e6eb8cc16c59f29dedc3af2c77cfd56f25 true
|
|
||||||
check_key 5d0f82319676d4d3636ff5dc2a38ea5ec8aeaac4835fdcab983ab35d76b7967b false
|
|
||||||
check_key cafcc75e94a014115f25c23aaae86e67352f928f468d4312b92240ff0f3a4481 false
|
|
||||||
check_key 3e5fdd8072574218f389d018e959669e8ca4ef20b114ea7dce7bfb32339f9f42 true
|
|
||||||
check_key 591763e3390a78ccb529ceea3d3a97165878b179ad2edaa166fd3c78ec69d391 true
|
|
||||||
check_key 7a0a196935bf79dc2b1c3050e8f2bf0665f7773fc07511b828ec1c4b1451d317 false
|
|
||||||
check_key 9cf0c034162131fbaa94a608f58546d0acbcc2e67b62a0b2be2ce75fc8c25b9a false
|
|
||||||
check_key e3840846e3d32644d45654b96def09a5d6968caca9048c13fcaab7ae8851c316 false
|
|
||||||
check_key a4e330253739af588d70fbda23543f6df7d76d894a486d169e5fedf7ed32d2e2 false
|
|
||||||
check_key cfb41db7091223865f7ecbdda92b9a6fb08887827831451de5bcb3165395d95d true
|
|
||||||
check_key 3d10bd023cef8ae30229fdbfa7446a3c218423d00f330857ff6adde080749015 false
|
|
||||||
check_key 4403b53b8d4112bb1727bb8b5fd63d1f79f107705ffe17867704e70a61875328 false
|
|
||||||
check_key 121ef0813a9f76b7a9c045058557c5072de6a102f06a9b103ead6af079420c29 true
|
|
||||||
check_key 386204cf473caf3854351dda55844a41162eb9ce4740e1e31cfef037b41bc56e false
|
|
||||||
check_key eb5872300dc658161df469364283e4658f37f6a1349976f8973bd6b5d1d57a39 true
|
|
||||||
check_key b8f32188f0fc62eeb38a561ff7b7f3c94440e6d366a05ef7636958bc97834d02 false
|
|
||||||
check_key a817f129a8292df79eef8531736fdebb2e985304653e7ef286574d0703b40fb4 false
|
|
||||||
check_key 2c06595bc103447b9c20a71cd358c704cb43b0b34c23fb768e6730ac9494f39e true
|
|
||||||
check_key dd84bc4c366ced4f65c50c26beb8a9bc26c88b7d4a77effbb0f7af1b28e25734 false
|
|
||||||
check_key 76b4d33810eed637f90d49a530ac5415df97cafdac6f17eda1ba7eb9a14e5886 true
|
|
||||||
check_key 926ce5161c4c92d90ec4efc58e5f449a2c385766c42d2e60af16b7362097aef5 false
|
|
||||||
check_key 20c661f1e95e94a745eb9ec7a4fa719eff2f64052968e448d4734f90952aefee false
|
|
||||||
check_key 671b50abbd119c756010416e15fcdcc9a8e92eed0f67cbca240c3a9154db55c0 false
|
|
||||||
check_key df7aeee8458433e5c68253b8ef006a1c74ce3aef8951056f1fa918a8eb855213 false
|
|
||||||
check_key 70c81a38b92849cf547e3d5a6570d78e5228d4eaf9c8fdd15959edc9eb750daf false
|
|
||||||
check_key 55a512100b72d4ae0cfc16c75566fcaa3a7bb9116840db1559c71fd0e961cc36 false
|
|
||||||
check_key dbfbec4d0d2433a794ad40dc0aea965b6582875805c9a7351b47377403296acd true
|
|
||||||
check_key 0a7fe09eb9342214f98b38964f72ae3c787c19e5d7e256af9216f108f88b00a3 true
|
|
||||||
check_key a82e54681475f53ced9730ee9e3a607e341014d9403f5a42f3dbdbe8fc52e842 true
|
|
||||||
check_key 4d1f90059f7895a3f89abf16162e8d69b399c417f515ccb43b83144bbe8105f6 true
|
|
||||||
check_key 94e5c5b8486b1f2ff4e98ddf3b9295787eb252ba9b408ca4d7724595861da834 false
|
|
||||||
check_key d16e3e8dfa6d33d1d2db21c651006ccddbf4ce2e556594de5a22ae433e774ae6 false
|
|
||||||
check_key a1b203ec5e36098a3af08d6077068fec57eab3a754cbb5f8192983f37191c2df false
|
|
||||||
check_key 5378bb3ec8b4e49849bd7477356ed86f40757dd1ea3cee1e5183c7e7be4c3406 false
|
|
||||||
check_key 541a4162edeb57130295441dc1cb604072d7323b6c7dffa02ea5e4fed1d2ee9e true
|
|
||||||
check_key d8e86e189edcc4b5c262c26004691edd7bd909090997f886b00ed4b6af64d547 false
|
|
||||||
check_key 18a8731d1983d1df2ce2703b4c85e7357b6356634ac1412e6c2ac33ad35f8364 false
|
|
||||||
check_key b21212eac1eb11e811022514c5041233c4a07083a5b20acd7d632a938dc627de true
|
|
||||||
check_key 50efcfac1a55e9829d89334513d6d921abeb237594174015d154512054e4f9d1 true
|
|
||||||
check_key 9c44e8bcba31ddb4e67808422e42062540742ebd73439da0ba7837bf26649ec4 true
|
|
||||||
check_key b068a4f90d5bd78fd350daa129de35e5297b0ad6be9c85c7a6f129e3760a1482 false
|
|
||||||
check_key e9df93932f0096fcf2055564457c6dc685051673a4a6cd87779924be5c4abead true
|
|
||||||
check_key eddab2fc52dac8ed12914d1eb5b0da9978662c4d35b388d64ddf8f065606acaf true
|
|
||||||
check_key 54d3e6b3f2143d9083b4c98e4c22d98f99d274228050b2dc11695bf86631e89f true
|
|
||||||
check_key 6da1d5ef1827de8bbf886623561b058032e196d17f983cbc52199b31b2acc75b true
|
|
||||||
check_key e2a2df18e2235ebd743c9714e334f415d4ca4baf7ad1b335fb45021353d5117f true
|
|
||||||
check_key f34cb7d6e861c8bfe6e15ac19de68e74ccc9b345a7b751a10a5c7f85a99dfeb6 false
|
|
||||||
check_key f36e2f5967eb56244f9e4981a831f4d19c805e31983662641fe384e68176604a true
|
|
||||||
check_key c7e2dc9e8aa6f9c23d379e0f5e3057a69b931b886bbb74ded9f660c06d457463 true
|
|
||||||
check_key b97324364941e06f2ab4f5153a368f9b07c524a89e246720099042ad9e8c1c5b false
|
|
||||||
check_key eff75c70d425f5bba0eef426e116a4697e54feefac870660d9cf24c685078d75 false
|
|
||||||
check_key 161f3cd1a5873788755437e399136bcbf51ff5534700b3a8064f822995a15d24 false
|
|
||||||
check_key 63d6d3d2c21e88b06c9ff856809572024d86c85d85d6d62a52105c0672d92e66 false
|
|
||||||
check_key 1dc19b610b293de602f43dca6c204ce304702e6dc15d2a9337da55961bd26834 false
|
|
||||||
check_key 28a16d02405f509e1cfef5236c0c5f73c3bcadcd23c8eff377253941f82769db true
|
|
||||||
check_key 682d9cc3b65d149b8c2e54d6e20101e12b7cf96be90c9458e7a69699ec0c8ed7 false
|
|
||||||
check_key 0000000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0000000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0100000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0100000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 0200000000000000000000000000000000000000000000000000000000000000 false
|
|
||||||
check_key 0200000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 0300000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0300000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0400000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0400000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0500000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0500000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0600000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0600000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0700000000000000000000000000000000000000000000000000000000000000 false
|
|
||||||
check_key 0700000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 0800000000000000000000000000000000000000000000000000000000000000 false
|
|
||||||
check_key 0800000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 0900000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0900000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0a00000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0a00000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0b00000000000000000000000000000000000000000000000000000000000000 false
|
|
||||||
check_key 0b00000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 0c00000000000000000000000000000000000000000000000000000000000000 false
|
|
||||||
check_key 0c00000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 0d00000000000000000000000000000000000000000000000000000000000000 false
|
|
||||||
check_key 0d00000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 0e00000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0e00000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 0f00000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 0f00000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 1000000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 1000000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 1100000000000000000000000000000000000000000000000000000000000000 false
|
|
||||||
check_key 1100000000000000000000000000000000000000000000000000000000000080 false
|
|
||||||
check_key 1200000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 1200000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key 1300000000000000000000000000000000000000000000000000000000000000 true
|
|
||||||
check_key 1300000000000000000000000000000000000000000000000000000000000080 true
|
|
||||||
check_key daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key dcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key dcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key ddffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key ddffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key deffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key deffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key dfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key dfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key e0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key e0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key e1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key e1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key e2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key e2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key e3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key e3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key e4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key e4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key e5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key e5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key e6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key e6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key e7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key e7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key e8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key e8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key e9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key e9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key eaffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key eaffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
|
||||||
check_key ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
|
||||||
check_key ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key f9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key f9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key faffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key faffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key fbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key fbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key fcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key fcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key fdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key fdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key feffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key feffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
check_key ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
|
||||||
check_key ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
|
||||||
hash_to_ec da66e9ba613919dec28ef367a125bb310d6d83fb9052e71034164b6dc4f392d0 52b3f38753b4e13b74624862e253072cf12f745d43fcfafbe8c217701a6e5875
|
|
||||||
hash_to_ec a7fbdeeccb597c2d5fdaf2ea2e10cbfcd26b5740903e7f6d46bcbf9a90384fc6 f055ba2d0d9828ce2e203d9896bfda494d7830e7e3a27fa27d5eaa825a79a19c
|
|
||||||
hash_to_ec ed6e6579368caba2cc4851672972e949c0ee586fee4d6d6a9476d4a908f64070 da3ceda9a2ef6316bf9272566e6dffd785ac71f57855c0202f422bbb86af4ec0
|
|
||||||
hash_to_ec 9ae78e5620f1c4e6b29d03da006869465b3b16dae87ab0a51f4e1b74bc8aa48b 72d8720da66f797f55fbb7fa538af0b4a4f5930c8289c991472c37dc5ec16853
|
|
||||||
hash_to_ec ab49eb4834d24db7f479753217b763f70604ecb79ed37e6c788528720f424e5b 45914ba926a1a22c8146459c7f050a51ef5f560f5b74bae436b93a379866e6b8
|
|
||||||
hash_to_ec 5b79158ef2341180b8327b976efddbf364620b7e88d2e0707fa56f3b902c34b3 eac991dcbba39cb3bd166906ab48e2c3c3f4cd289a05e1c188486d348ede7c2e
|
|
||||||
hash_to_ec f21daa7896c81d3a7a2e9df721035d3c3902fe546c9d739d0c334ed894fb1d21 a6bedc5ffcc867d0c13a88a03360c8c83a9e4ddf339851bd3768c53a124378ec
|
|
||||||
hash_to_ec 3dae79aaca1abe6aecea7b0d38646c6b013d40053c7cdde2bed094497d925d2b 1a442546a35860a4ab697a36b158ded8e001bbfe20aef1c63e2840e87485c613
|
|
||||||
hash_to_ec 3d219463a55c24ac6f55706a6e46ade3fcd1edc87bade7b967129372036aca63 b252922ab64e32968735b8ade861445aa8dc02b763bd249bff121d10829f7c52
|
|
||||||
hash_to_ec bc5db69aced2b3197398eaf7cf60fd782379874b5ca27cb21bd23692c3c885cc ae072a43f78a0f29dc9822ae5e70865bbd151236a6d7fe4ae3e8f8961e19b0e5
|
|
||||||
hash_to_ec 98a6ed760b225976f8ada0579540e35da643089656695b5d0b8c7265a37e2342 6a99dbfa8ead6228910498cc3ff3fb18cb8627c5735e4b8657da846c16d2dcad
|
|
||||||
hash_to_ec e9cdc9fd9425a4a2389a5d60f76a2d839f0afbf66330f079a88fe23d73eae930 8aa518d091928668f3ca40e71e14b2698f6cae097b8120d7f6ae9afba8fd3d60
|
|
||||||
hash_to_ec a50c026c0af2f9f9884c2e9b8464724ac83bef546fec2c86b7de0880980d24fb b07433f8df39da2453a1e13fd413123a158feae602d822b724d42ef6c8e443bf
|
|
||||||
hash_to_ec bf180e20d160fa23ccfa6993febe22b920160efc5a9614245f1a3a360076e87a 9d6454ff69779ce978ea5fb3be88576dc8feaedf151e93b70065f92505f2e800
|
|
||||||
hash_to_ec b2b64dfeb1d58c6afbf5a56d8c0c42012175ebb4b7df30f26a67b66be8c34614 0523b22e7f220c939b604a15780abc5816709b91b81d9ee1541d44bd2586bbd8
|
|
||||||
hash_to_ec 463fc877f4279740020d10652c950f088ebdebeae34aa7a366c92c9c8773f63a daa5fa72e70c4d3af407b8f2f3364708029b2d4863bbdde54bd67bd08db0fcad
|
|
||||||
hash_to_ec 721842f3809982e7b96a806ae1f162d98ae6911d476307ad1e4f24522fd26f55 4397c300a8cfcb42e7cc310bc975dc975ec2d191eaa7e0462998eb2830c34126
|
|
||||||
hash_to_ec 384da8d9b83972af8cbefc2da5efc744037c8ef40efa4b3bacc3238a6232963d 3c80f107e6868f73ef600ab9229a3f4bbe24f4adce52e6ab3a66d5d510e0670d
|
|
||||||
hash_to_ec e26f8adef5b6fe5bb01466bff0455ca23fda07e200133697b3b6430ca3332bde e262a58bcc1f8baf1980e00d5d40ba00803690174d14fb4c0f608429ce3df773
|
|
||||||
hash_to_ec 6e275b4ea4f085a5d3151aa08cf16a8c60b078e70be7ce5dac75b5d7b0eebe7c cb21b5a7744b4fcdc92ead4be0b04bcb9145e7bb4b06eff3bb2f0fe429b85108
|
|
||||||
hash_to_ec a0dde4561ad9daa796d9cd8a3c34fd41687cee76d128bf2e2252466e3ef3b068 79a2eb06bb7647f5d0aae5da7cf2e2b2d2ce890f25f2b1f81bfc5fef8c87a7d3
|
|
||||||
hash_to_ec dbaf63830e037b4c329969d1d85e58cb6c4f56014fd08eb38219bd20031ae27c 079c93ae27cd98075a487fd3f7457ad2fb57cdf12ec8651fedd944d765d07549
|
|
||||||
hash_to_ec 1e87ba8a9acf96948bc199ae55c83ab3277be152c6d0b1d68a07955768d81171 5c6339f834116791f9ea22fcc3970346aaeddacf13fbd0a7d4005fbd469492ca
|
|
||||||
hash_to_ec 5a544088e63ddf5b9f444ed75a75bc9315c4c50439522f06b4823ecaf5e8a08d e95ca0730d57c6469be3a0f3c94382f8490257e2e546de86c650bdbc6482eaee
|
|
||||||
hash_to_ec e4e06d92ebb036a5e4bb547dbaa43fd70db3929eef2702649455c86d7e59aa46 e26210ff8ee28e24ef2613df40aa8a874b5e3c1d07ae14acc59220615aa334dc
|
|
||||||
hash_to_ec 5793b8b32dcc0f204501647f2976493c4f8f1fa5132315226f99f29a5a6fdfce 656e390086906d99852c9696e831f62cb56fc8f85f9a5c936c327f23c7faf4fe
|
|
||||||
hash_to_ec 84f56fa4d7f12e0efd48b1f7c81c15d6e3843ebb419f4a27ec97028d4f9da19e 0cbd4f0cd288e1e071cce800877de6aef97b63fff867424a4f2b2bab25602608
|
|
||||||
hash_to_ec 242683ddf0a9fc55f6585de3aa64ea17c9c544896ff7677cd82c98f833bdf2ca 38c36d52314549213df7c7201ab7749a4724cbea92812f583bb48cabc20816ad
|
|
||||||
hash_to_ec a93ee320dc030aa382168c2eb6d75fce6e5a63a81f15632d514c6de8a7cfa5ee bd0a2facaa95bc95215a94be21996e46f789ee8beb38e75a1173b75fc686c505
|
|
||||||
hash_to_ec e36136601d84475d25c3f14efe030363d646658937a8a8a19a812d5e6deb5944 2fb93d78fae299c9f6b22346acfb829796ee7a47ec71db5456d8201bec6c35a3
|
|
||||||
hash_to_ec ba4b67d3d387c66baa4a32ec8b1db7681087e85076e71bab10036388c3aeb011 cc01329ce56f963bf444a124751c45b2c779ccb6dea16ca05251baca246b5401
|
|
||||||
hash_to_ec 3fbc91896a2585154d6f7094c5ab9c487e29a27951c226eec1235f618e44946b 7d983acbb901bf5497d0708392e5e742ec8c8036cbb0d03403e9929da8cc85a7
|
|
||||||
hash_to_ec a2da289fed650e9901f69a5f33535eb47c6bd07798633cbf6c00ce3172df76ac dca8a4d30ec2d657fefd0dba9c1c5fd45a79f665048b3cf72ac2c3b7363da1ac
|
|
||||||
hash_to_ec 99025d2d493f768e273ed66cacd3a5b392761e6bd158ca09c8fba84631ea1534 7ef5af79ab155ab7e1770a47fcd7f194aca43d79ec6e303c7ce18c6a20279b04
|
|
||||||
hash_to_ec 3cf1d01d0b70fb31f2a2f979c1bae812381430f474247d0b018167f2a2cd9a9f 7c53d799ec938a21bb305a6b5ca0a7a355fa9a68b01d289c4f22b36ce3738f95
|
|
||||||
hash_to_ec 639c421b49636b2a1f8416c5d6e64425fe51e3b52584c265502379189895668e 0b47216ae5e6e03667143a6cf8894d9d73e3152c64fb455631d81a424410e871
|
|
||||||
hash_to_ec 4ccf2c973348b7cc4b14f846f9bfcdcb959b7429accf6dede96248946841d990 7fd41f5b97ba42ed03947dd953f8e69770c92cc34b16236edad7ab3c78cbbb2e
|
|
||||||
hash_to_ec f76ae09fff537f8919fd1a43ff9b8922b6a77e9e30791c82cf2c4b8acb51363e 8e2c6bf86461ad2c230c496ee3896da33c11cc020fd4c70faa3645b329049234
|
|
||||||
hash_to_ec 98932da7450f15db6c1eef78359904915c31c2aa7572366ec8855180edb81e3a 86180adddfac0b4d1fb41d58e98445dde1da605b380d392e9386bd445f1d821c
|
|
||||||
hash_to_ec ab26a1660988ec7aba91fc01f7aa9a157bbc12927f5b197062b922a5c0c7f8dd 2c44a43eda0d0aad055f18333e761f2f2ec11c585ec7339081c19266af918e4f
|
|
||||||
hash_to_ec 4465d0c1b4930cc718252efd87d11d04162d2a321b9b850c4a19a6acdfca24f4 b03806287d804188a4d679a0ecee66f399d7bdc3bd1494f9b2b0772bbb5a034f
|
|
||||||
hash_to_ec 0f2a7867864ed00e5c40082df0a0b031c89fa5f978d9beb2fde75153f51cfb75 5c471e1b118ef9d76c93aec70e0578f46e8db1d55affd447c1f64c0ad9a5caa5
|
|
||||||
hash_to_ec 5c2808c07d8175f332cae050ce13bec4254870d76abff68faf34b0b8d3ad5000 eeff1d9a5aa428b7aecc575e63dde17294072eb246568493e1ed88ce5c95b779
|
|
||||||
hash_to_ec 36300a21601fad00d00da45e27b36c11923b857f97e50303bd01f21998eaef95 b33b077871e6f5dad8ff6bc621c1b6dedcf700777d996c8c02d73f7297108b7e
|
|
||||||
hash_to_ec 9e1afb76d6c480816d2cedd7f2ab08a36c309efaa3764dcdb51bad6049683805 4cd96ba7b543b1a224b8670bf20b3733e3910711d32456d3e58e920215788adf
|
|
||||||
hash_to_ec 685f152704664495459b76c81567a4b571e8b307dd0e3c9b08ee95651a006047 80dd6b637580cb3be76025867f1525852b65a7a66066993fda3af7eb187dc1a5
|
|
||||||
hash_to_ec 0b216444391a1163c14f7b27f9135e9747978c0e426dce1fa65c657f3e9146be 021259695a6854a4a03e8c74d09ab9630a401bfca06172a733fe122f01af90b4
|
|
||||||
hash_to_ec cfcb35e98f71226c3558eaa9cf620db5ae207ece081ab13ddea4b1f122850a5a 46763d2742e2cdffe80bb3d056f4d3a1565aa83f19aab0a1f89e54ad81ae0814
|
|
||||||
hash_to_ec 07e7292da8cdcdb58ee30c3fa16f1d609e9b3b1110dd6fa9b2cc18f4103a1c12 fe949ca251ac66f13a8925ae624a09cdbf6696d3c110442338d37700536e8ec7
|
|
||||||
hash_to_ec 813bc7e3749e658190cf2a4e358bc07a6671f262e2c4eef9f44c66066a72e6a7 6b92fbda984bd0e6f4af7a5e04c2b66b6f0f9d197a9694362a8556e5b7439f8a
|
|
||||||
hash_to_ec 89c50a1e5497156e0fae20d99f5e33e330362b962c9ca00eaf084fe91aaec71d ef36cb75eb95fb761a8fa8c376e9c4447bcd61421250f7a711bd289e6ed78a9b
|
|
||||||
hash_to_ec d9bd9ff2dd807eb25de7c5de865dbc43cce2466389cedbc92b90aab0eb014f81 30104771ff961cd1861cd053689feab888c57b8a4a2e3989646ea7dea40f3c04
|
|
||||||
hash_to_ec b8c837501b6ca3e118db9848717c847c062bf0ebeca5a7c211726c1426878af5 19a1e204b4a32ce9cccf5d96a541eb76a78789dceaf4fe69964e58ff96c29b63
|
|
||||||
hash_to_ec 84376c5350a42c07ac9f96e8d5c35a8c7f62c639a1834b09e4331b5962ecace8 ba1e4437d5048bd1294eadc502092eafc470b99fde82649e84a52225e68e88f2
|
|
||||||
hash_to_ec a3345e4a4cfc369bf0e7d11f49aed0d2a6ded00e3ff8c7605db9a919cf730640 0d318705c16e943c0fdcde134aaf6e4ccce9f3d9161d001861656fc7ea77a0b1
|
|
||||||
hash_to_ec 3c994dfb9c71e4f401e65fd552dc9f49885f88b8b3588e24e1d2e9b8870ffab1 984157de5d7c2c4b43b2bffea171809165d7bb442baea88e83b27f839ebdb939
|
|
||||||
hash_to_ec 153674c1c1b18a646f564af77c5bd7de452dc3f3e1e2326bfe9c57745b69ec5c e9a4a1e225ae472d1b3168c99f8ba1943ad2ed84ef29598f3f96314f22db9ef2
|
|
||||||
hash_to_ec 2d46a705d4fe5d8b5a1f4e9ef46d9e06467450eb357b6d39faa000995314e871 b9d1aec540bf6a9c0e1b325ab87d4fbe66b1df48986dde3cb62e66e136eba107
|
|
||||||
hash_to_ec 6764c3767f16ec8faecc62f9f76735f76b11d7556aeb61066aeaeaad4fc9042f 3a5c68fb94b023488fb5940e07d1005e7c18328e7a84f673ccd536c07560a57b
|
|
||||||
hash_to_ec c99c6ee5804d4b13a445bc03eaa07a6ef5bcb2fff0f71678dd3bd66b822f8be8 a9e1ce91deed4136e6e53e143d1c0af106abde9d77c066c78ebbf5d227f9dde0
|
|
||||||
hash_to_ec 3009182e1efac085c7eba24a7d9ef28ace98ebafa72211e73a41c935c37e6768 e55431a4c89d38bd95f8092cdf6e44d164ad5855677aba17ec262abc8c217c86
|
|
||||||
hash_to_ec e7153acd114a7636a207be0b67fa86fee56dd318f2808a81e35dd13d4251b2d0 ff2b98d257e4d4ff7379e8871441ca7d26e73f78f3f5afcf421d78c9799ba677
|
|
||||||
hash_to_ec 6378586744b721c5003976e3e18351c49cd28154c821bc45338892e5efedd197 3d765fb7bb4e165a3fa6ea00b5b5e22250f3861f0db0099626d9a9020443dda2
|
|
||||||
hash_to_ec 5be49aba389b7e3ad6def3ba3c7dbec0a11a3c36fc9d441130ef370b8a8d29c2 2d61faf38062dc98ae1aaafec05e90a925c9769df5b8b8f7090d9e91b2a11151
|
|
||||||
hash_to_ec f7bc382178d38e1b9a1a995bd8347c1283d8a2e8d150379faa53fd125e903d2b 544c815da65c3c5994b0ac7d6455578d03a2bc7cf558b788bcdb3430e231635a
|
|
||||||
hash_to_ec c28b5c4b6662eebb3ec358600644849ebeb59d827ed589c161d900ca18715fa8 a2d64db3c0e0353c257aadf9abc12ac779654d364f348b9f8e429aa7571203db
|
|
||||||
hash_to_ec 3a4792e5df9b2416a785739b9cf4e0d68aef600fa756a399cc949dd1fff5033a 4b54591bd79c30640b700dfb7f20158f692f467b6af70bd8a4e739c14a66c86a
|
|
||||||
hash_to_ec 002e70f25e1ceaf35cc14b2c6975a4c777b284a695550541e6f5424b962c19f5 73987e9342e338eb57a7a9e03bd33144db37c1091e952a10bd243c5bb295c18a
|
|
||||||
hash_to_ec 7eb671319f212c9cae0975571b6af109124724ba182937a9066546c92bdeff0c 49b46da3be0df1d141d2a323d5af82202afa2947a95b9f3df47722337f0d5798
|
|
||||||
hash_to_ec ca093712559c8edd5c51689e2ddcb8641c2960e5d9c8b03a44926bb798a0c8dc b9ef9cf0f8e4a3d123db565afafb1102338bfb75498444ac0a25c5ed70d615da
|
|
||||||
hash_to_ec cfea0a08a72777ff3aa7be0d8934587fa4127cd49a1a938232815dc3fd8b23ac b4de604b3d712f1ef578195fb0e53c865d41e2dfe425202c6cfe6f10e4404eb5
|
|
||||||
hash_to_ec aa0122ae258d6db21a26a31c0c92d8a0e3fdb46594aed41d561e069687dedcd6 5247eaec346de1c6cddf0ab04c12cd1d85cdb6d3a2fba2a5f9a5fe461abef5eb
|
|
||||||
hash_to_ec b3941734f4d3ba34ccaf03c4c737ac5a1e036eb74309300ce44d73aca24fef08 535938985c936e3780c61fe29a4121d6cb89a05080b6c2147031ea0c2b5b9829
|
|
||||||
hash_to_ec 8c2ee1041a2743b30dcbf413cc9232099b9268f82a5a21a09b63e7aff750882f 6ad0d4b3a65b522dfad0e9ac814b1fb939bc4910bd780943c72f57f362754cca
|
|
||||||
hash_to_ec 4b6829a2a2d46c8f0d0c23db0f735fcf976524bf39ccb623b919dd3b28ad5193 2e0097d7f92993bc45ba06baf4ca63d64899d86760adc4eb5eeefb4a78561050
|
|
||||||
hash_to_ec 9c1407cb6bba11e7b4c1d274d772f074f410d6fe9a1ee7a22cddf379257877d9 692261c7d6a9a7031c67d033f6d82a68ef3c27bd51a5666e55972238769821cd
|
|
||||||
hash_to_ec 638c42e4997abf8a4a9bffd040e31bd695d590cde8afbd7efd16ffdbae63bf66 793024c8ce196a2419f761dde8734734af6bd9eb772b30cc78f2cb89598dce97
|
|
||||||
hash_to_ec 1fb60d79600de151a1cf8a2334deb5828632cbd91cb5b3d45ae06e08187ae23d ff2542cde5bc2562e69471a31cfc3d0c26e2f6ccc1891a633b07a3968e42521c
|
|
||||||
hash_to_ec d2fdbbae4e38a1b734151c3df52540feb2d3ff74edfef2f740e49a5c363406ee 344c83ba6ff4e38b257077623d298d2f2b52002645021241bc9389f81b29ad12
|
|
||||||
hash_to_ec 836c27a6ddfe1a24aba3d6022dff6dfe970f142d8b4ac6afb8efcba5a051942f b8af481d33726b3f875268282d621e4c63f891a09f920b8f2f49080f3a507387
|
|
||||||
hash_to_ec 46281153ddcdf2e79d459693b6fe318c1969538dd59a750b790bfff6e9481abf 8eaf534919ab6573ba4e0fbde0e370ae01eae0763335177aa429f61c4295e9d4
|
|
||||||
hash_to_ec d57b789e050bf3db462b79a997dac76aa048d4be05f133c66edee56afd3dbe66 0c5a294cb2cbb6d9d1c0a1d57d938278f674867f612ed89dcbe4533449f1a131
|
|
||||||
hash_to_ec 548d524d03ac22da18ff4201ce8dbee83ad9af54ee4e26791d26ed2ab8f9bfc7 c6609d9e7d9fd982dec8a166ff4fb6f7d195b413aad2df85f73d555349134f3b
|
|
||||||
hash_to_ec cc920690422e307357f573b87a6e0e65f432c6ec12a604eb718b66ba18897a56 6f11c466d1c72fccd81e51d9bda03b6e8d6a395e1d931b2a84e392dc9a3efa18
|
|
||||||
hash_to_ec c7fb8a51f5fcd8824fc0875d4eb57ab4917cb97090a6e2288f852f2bb449edd9 45543fea6eed461016e48598b521f18ff70178afea18032b188deea3e56052fc
|
|
||||||
hash_to_ec c681bb1b829e24b1c52cb890036b89f0029d261c6a15e5b2c684ee7dfe91e746 263006fe2c6b08f1ab29cdf442472c298e2faf225bbf5c32399d3745cd3904bd
|
|
||||||
hash_to_ec e06411c542312fdd305e17e46be14c63bab5836dc8751da06164b1ae22d4e20f 901871be7a7ff5aecade2acff869846f3c50de69307ac155f2aa3a74d5472ef2
|
|
||||||
hash_to_ec 9c725a2acb80fa712f9781da510e5163b1b30f4e1c064c26b5185e537f0614ea 02420d49257846eb39fddd196d3171679f6be21d9adac667786b65a6e90f57b1
|
|
||||||
hash_to_ec 22792772820feafa85c5cb3fa8f876105251bef08617d389619697f47dff54f2 a3ad444e7811693687f3925e7c315ae55d08d9f4b0a29876bc2a891ab941c1c3
|
|
||||||
hash_to_ec 0587b790121395d0f4f39093d10b4817f58a1e80621a24eea22b3c127d6ac5a2 86c417c695c64c7becaad0d59ddbb2bca4cb2b409a21253d680aac1a08617095
|
|
||||||
hash_to_ec fa0b5f28399bef0cd87bfe6b8a2b69e9c5506fb4bacd22deba8049615a5db526 ede0ea240036ff75d075258a053f3ce5d6f77925d358dbe33c06509fc9b12111
|
|
||||||
hash_to_ec 62a3274fc0bed109d5057b865c2ba6b6a5a417cb90a3425674102fcd457ede2d ff7e46751bb4dcd1e800a8feab7cf6771f42dc0cfed7084c23b8a5d255a6f34e
|
|
||||||
hash_to_ec a6fcd4aecaaaf281563b9b7cd6fbc7b1829654f644f4165942669a2ef632b2bf 28f136be0eb957a5b36f8ec294399c9f73ad3a3c9bb953ad191758ced554a233
|
|
||||||
hash_to_ec 01baa4c06d6676c9b286cda76ed949fd80a408b3309500ba84a5bb7e3dce58e2 a943d1afa2efce284740e7db21ea02db70b124808be2ff80cbf9b9cb96c7b73e
|
|
||||||
hash_to_ec dd9aff9c006ba514cef8fae665657bc9813fe2715467cf479643ea4c4e365d6d 68de2f7d49de4004286ce0989a06a686b15d0f463a02ffd448a18914e1ddf713
|
|
||||||
hash_to_ec 3df3513d5e539161761ce7992ab9935f649bc934bed0da3c5e1095344b733bb9 e9c2dd747d7b2482474325943cd850102b8093164678362c7621993a790e2a8a
|
|
||||||
hash_to_ec 7680cfb244dc8ef37c671fff176be1a3dad00e5d283f93145d0cbee74cca2df4 a0fd8c3cca16a130eaa5864cbe8152b7adfbf09e8cf72244b2fc8364c3b20bf4
|
|
||||||
hash_to_ec 8a547c38bd6b219ea0d612d4a155eba9c56034a1405dcf4b608de787f37e0fd8 76bf0dc40fd0a5508c5e091d8bb7eccfa28b331e72c6a0d4ac0e05a3d651850b
|
|
||||||
hash_to_ec dd93901621f58465e9791012afa76908f1e80ad80e52b809dc7fc32bb004f0a8 09a0b7ecfe8058b1e9ee01c9b523826867ca97a32efad29ac8ceebca67a4ea00
|
|
||||||
hash_to_ec b643010220f1f4ee6c7565f6e1b3dc84c18274ede363ac36b6af3707e69a1542 233c9ff8de59e5f96c2f91892a71d9d93fa7316319f30d1615f10ac1e01f9285
|
|
||||||
hash_to_ec c2637b2299dfc1fd7e953e39a582bafd19e6e7fff3642978eb092b900dbfea80 339587ba1c05e2cba44196a4be1fd218b772199e2c61c3c0ff21dcd54b570c43
|
|
||||||
hash_to_ec 1f36d3a7e7c468eb000937de138809e381ad2e23414cbbaac49b7f33533ed486 7e5b0a96051c77237a027a79764c2763487af88121c7774645e97827fb744888
|
|
||||||
hash_to_ec 8c142a55f60b2edbe03335b7f90aa2bd63e567048a65d61c70cb28779c5200af d3d6d5563b3d81c8c91cf9806bb13b2850fb7c162c610fd2f5b83c464add8182
|
|
||||||
hash_to_ec 99e7b98293c9de1f81aff1376485a990014b8b176521b2a68cdbde6300190398 119cbc01a1d9b9fb4759031d3a70685aebea0f01bc5ee082ce824265fd21b3b4
|
|
||||||
hash_to_ec 9753bd38be072b51490290be6207ca4545e3541bdf194e0850ae0a9f9e64b8ba 1ad3aa759863153606fa6570f0e1290baded4c8c1f2ba0f67c1911bfc8ccd7a0
|
|
||||||
hash_to_ec 322703864ceee19b7f17cec2a822f310f0c4da3ff98b0be61a6fd30ac4db649c 89d9e7a5947e1cde874e4030de278070aae363063cd3592ce5411821474f0816
|
|
||||||
hash_to_ec c1acd01e1e535fad273a8b757d981470f43dd7d95af732901fbba16b6e245761 57e80445248111150da5e63c706b4abbf3eef2cc508bd0347ff6b81e8c59f5bc
|
|
||||||
hash_to_ec 492473559f181bbe78f60215bc6d3a5168435ea2fc0a508372d6f5ca126e9767 df3965f137cf6f60c56ebd7c8f246281fd6dc92ce23a37e9f846f8452c884e01
|
|
||||||
hash_to_ec afa9d6e0e2fb972ee806beb450c2c0165e58234b0676a4ec0ca19b6e710d7c35 669a57e69dd2845a5e50ed8e5d8423ac9ae792a43c7738554d6c5e765a7b088a
|
|
||||||
hash_to_ec 094de050bdadef3b7dbaeeca29381c667e63e71220970149d97b95db8f4db61b 0cf5d03530c5e97850d0964c6a394de9cde1e8e498f8c0e173c518242c07f99a
|
|
||||||
hash_to_ec 2ce583724bc699ad800b33176a1d983512fe3cb3afa65d99224b23dae223efb7 e1548fd563c75ae5b5366dbab4cb73c54e7d5e087c9e5453125ff8fbe6c83a5c
|
|
||||||
hash_to_ec 8064974b976ff5ef6adaade6196ab69cda6970cd74f7f5899181805f691ad970 98ae63c47331a4ac433cb2f17230c525982d89d21e2838515a36ec5744ec2d15
|
|
||||||
hash_to_ec 384911047de609c6ae8438c745897357989363885cef2381a8a00a090cf04a58 4692ec3a0a03263620841c108538d584322fdd24d221a74bf1e1f407f83828af
|
|
||||||
hash_to_ec 0e1b1ced5ae997ef9c10b72cfc6d8c36d7433c01fc04f4083447f87243282528 6ee443ab0637702b7340bd4a908b9e2e63df0cc423c409fb320eb3f383118b80
|
|
||||||
hash_to_ec 5a7aea70c85c040af6ff3384bcaa63ec45c015b55b44fffa37ab982a00dc57c5 2df2e20137cefd166c767646ecd2e386d28f405aebe43d739aa55beba04ed407
|
|
||||||
hash_to_ec 3e878a3567487f20f7c98ea0488a40b87f1ba99e50bbfe9f00a423f927cbd898 697c7e60e4bf8c429ba7ac22b11a4b248d7465fc6abe597ec6d1e1c973330688
|
|
||||||
hash_to_ec c0bb08350d8a4bb6bf8745f6440e9bd254653102a81c79d6528da2810da758e4 396a872ac9147a69b27223bf4ec4198345b26576b3690f233b832395f2598235
|
|
||||||
hash_to_ec 6c3026a9284053a4ddb754818f9ae306ffa96eb7003bd03826eeccc9a0cf656e bef73da51d3ba9972a33d1afb7d263094b66ab6dbe3988161b08c17f8c69c2d5
|
|
||||||
hash_to_ec f80b7d8f5a80d321af3a42130db199d9edcb8f5a82507d8bfca6d002d65458b6 aa59c167ea60ee024421bfbd00adbb3cbfc20e16bd3c9b172a6bef4d47ca7f57
|
|
||||||
hash_to_ec bc0ffc24615aa02fafef447f17e7b776489cd2cc909f71e8344e01cad9f1610d 5c4195cc8dc3518143f06a9c228ae59ec9a6425a8fab89bfc638ad997cf35220
|
|
||||||
hash_to_ec b15fad558737229f8816fcba8fbef805bd420c03e392d118c69bdf01890c4924 f5810477e37554728837f097e1b170d1d8c95351c7fff8abbbfc624e1a50c1b9
|
|
||||||
hash_to_ec ec8c1f10d8e9da9cf0d57c4a1f2c402771bed7970109f3cf21ad32111f1f198f a697e0a3f09827b0cf3a4ffb6386388feda80d30ffffcbd54443dafcba162b28
|
|
||||||
hash_to_ec a989647bf0d70fdb7533b8c303a2a07f5e42e26a45ffc4e48cff5ba88643a201 450fd73e636f94d0d232600dd39031386b0e2ecde4105124fc451341da9803db
|
|
||||||
hash_to_ec 7159971b03c365480d91d625a0fadc8e3a632c518acf0dbec87dd659da70e168 377bc43c038ac46cf6565aa0a6d6bf39968c0c1142755dba3141eeebf0acdf5d
|
|
||||||
hash_to_ec e39089a64fedac4b2c25e36312b33f79d02bf75a883f450f910915b8560a3b06 77efa7db1be020e77596f550de45626824a8268095d56a0991696b211cb329cc
|
|
||||||
hash_to_ec 2056b3c6347611bb0929dad00ec932a4d9bec0f06b2d57f17e01ffa1528a719e b6072c2be2ce928e8cbbb87e8eb7e06975c0f93b309dd3b6a29edaad2b56f99b
|
|
||||||
hash_to_ec 2c026793146e81b889fc741d62e06c341ce263560d57cd46d0376f5b29174489 8f1f64b67762aa784969e954c196a2c6610addc3604aa3291eb0b80304dfe9ef
|
|
||||||
hash_to_ec be6026d6704379c489fa7749832b58bdb1a9685a5ffb68c438537f2f76e0011f 0072569a4090a9ad383a205bb092196c9de871c22506e3bb63d6b9d1b2357c96
|
|
||||||
hash_to_ec f4db802d5c6b7d7b53663b03d988b4cd0c7cad6c26612c5307754a93ebdc9710 f21bc9be4cb28761f6fe1d0a555ad5e9748375a2e9faea25a1df75cc8d273e18
|
|
||||||
hash_to_ec c27d79a564c56b00956a55090481e85fbc837fd5fb5e8311ecb436e300c07e3a 1b1891e6abec74621501450cd68bb1eeaa5b2fffff4ec441a55d1235ff3a0842
|
|
||||||
hash_to_ec a1e2f93c717cad32af386efa624198973df5a710963dd19d4c3ac40032a3a286 69c60571e3f9f63d2bfb359386ae3b8cd9e49a2e9127753002866e85c0443573
|
|
||||||
hash_to_ec 76920d7b1763474bc94a16433c3c28241a9acdee3ff2b2cb0e6757ba415310aa c1b409169f102b696fc7fa1aa9c48631e58e08b5132b6aadf43407627bb1b499
|
|
||||||
hash_to_ec 57ac654b29fa227c181fff2121491fcb283af6cbe932c8199c946862c0e90cb2 a204e8d327ea93b0b1bd74a78ffc370b20cea6455e209f2bc258114baa16d728
|
|
||||||
hash_to_ec 88e66cfaef6432b759c50efce885097d1752252b479dac5ed822fa6c85d56427 6fb84790d3749a5c1088209ee3823848d9c19bf1524215c44031143dd8080d70
|
|
||||||
hash_to_ec c1e55da929c4f8f793696fc77ff4e1c317c34852d98403bfd15dd388ee7df0df 2f41e76f15c5b480665bd84067e3b543b85ce6de02be9da7a550b5e1ead94d34
|
|
||||||
hash_to_ec 29e9ace5aa3c5a572b13f4b62b738a764d90c8c293ccb062ad798acbab7c5ef4 bce791aba1edc2a66079628fd838799489ab16b0a475ce7fe62e24cc56fe131c
|
|
||||||
hash_to_ec f25b2340689dadacaa9a0ef08aee8447d80b982e8a1ea42cf0500a1b9d85b37d f7f53aa117e6772a9abc452b3931b0a99405ac45147e7c550ac9fcf7ffe377b5
|
|
||||||
hash_to_ec 0cb6c47fc8478063b33f5aed615a05bcc84d782c497b6cc8e76ec1fa11edbfdb 7a0b58b03147e7c9be1d98de49ead2ce738d0071b0af8ca03cc92ceb26fc2246
|
|
||||||
hash_to_ec 7bd7287d7c4b596fe46fe57a6982c959653487bea843a77dd47d40986200d576 343084618c58284c64a5ff076f891be64885dc2ac73fa1567f7b39fde6b91542
|
|
||||||
hash_to_ec e4984bf330708152254fb18ecef12d546afd24898a3cf00fba866957b6ee1b82 c70e88b061656181fbd6ff12aca578fb66de5553c756ea4698a248b177185bc6
|
|
||||||
hash_to_ec cefd6c3cb9754ea632d6aea140af017de5ea12e5184f868936b74d9aa349d603 4b476502a8a483aadd50667f262f95351901628dd3a2aac1a5a41c4ea03f1647
|
|
||||||
hash_to_ec da5d0f33344ee7f3345204badf183491b9452b84bccc907602c7bad43e5cf43e 9561b9e61241625e028361494d4fa5cd78df4c7219fa64c8fede6d8421b8904a
|
|
||||||
hash_to_ec d6f0a4f8c770a1274a76fd7ae4e5faf7779249263e1aaecc6f815cf376f5c302 cd5c55820be10f0d38feb81363ede3716a9168601a0dd1ce3109aab81367d698
|
|
||||||
hash_to_ec b6bf32491d12a41c275d8518fc534d9a0d17aade509e7e8b8409a95c86167307 4aae534abbd67a9a8f2974154606c0e9be8932e920c7a5e931b46a92859acf82
|
|
||||||
hash_to_ec 0f930beaad041f9cefd867bc194027dd651fb3c9bda5944ececdba8a7136b6d3 521708f8149891b418d0920369569a9d578029c78f8e41c68a0bb68d3ad5df60
|
|
||||||
hash_to_ec 49b1fe0f97be74b81e0b047027b3e9f726fa5e90a67dafa877309397291c06c5 0852e59dfae5ec32cce606c119376597bce5cd4d04879d329f74e3ec66414cd3
|
|
||||||
hash_to_ec 4d57647d03f2cfbd4782fcc933e0683b52d35fc8d37283e6c7de522ddfa7e698 cbeb9ebfbbc49ec81fac3b7b063fecac1bb40ea686d3ffb08f82b291715cd87f
|
|
||||||
hash_to_ec 4ea3238c06fc9346c7421ff85bc0244b893860b94bc437378472814d09b2e99f a1fbae941adc344031bbdf53385dfdc012311490a4eb5e9a2749a21b27ce917a
|
|
||||||
hash_to_ec 0cd3609f5c78b318cb853d189b73b1ee2d00edd4e5fce2812027daa3fcb1fed1 0c7a7241b16e3c47d41f5abbf205797bd4b63fc425a7120cb2a4bf324e08ae74
|
|
||||||
hash_to_ec d74ab71428e36943c9868f70d3243469babd27988a1666a06f499a5741a52e3e 65b7c259f3b4547c082b2a7669b2b363668c4d87ac14e80471317b03b34e5216
|
|
||||||
hash_to_ec f6b151998365e7d69bcbce383dd2e8b5bf93b8b72f029ff942588208c1619591 6ce840ce5dfbca238665c1e6eddb8b045aa85c69b5976fc55ab57e66d3d0a791
|
|
||||||
hash_to_ec 207751de234b2bd7ec20bdd8326210c23aa68f04875c94ad7e256a96520f25d6 fc8f79ab3af317c38bfb88f40fb84422995a0479cfa6b03fa6df7f4e5f2813fb
|
|
||||||
hash_to_ec 62291e2873f38c0a234b77d1964205f3f91905c261d3c06f81051a9b0cb787cb 076d1d767457518e6777cb3bd4df22c8a19eb617e4bbccd1b0bd37522d6597a5
|
|
||||||
hash_to_ec 4b060df2d2854036751d00190ee821cb0066d256d4172539fdfa6fbd1cdfe1f9 59866e927c69e7de5df00dc46c0d2a1ddf799d901128ff040cebb8fd61b95da4
|
|
||||||
hash_to_ec ac8daf73f9c609bb36bce4fdeec1e50be5f22de38c3904fabcf758f0fc180bc7 7d8dc4e956363b652468a5fecafd7c08d48a2297e93b8edcb38e595fdd5a1fde
|
|
||||||
hash_to_ec fef7b6563fd27f3aab1d659806b26b8f2ec38bc8feefad50288383c001d1c20f e6e42547f12df431439d45103d2c5a583248f44554a98a3a433cf8c38b11805d
|
|
||||||
hash_to_ec 40a3d6871c76ecc6bb7b28324478733e196cc11d062dd4c9265cf31be5cf5a97 8c55a3811c241a020b1be202a58d5defbc4c8945d73b132570b47dd7c019ccf0
|
|
||||||
hash_to_ec 0cd71e7e562b2b47f4bc8640caf20e69d3a62f10231b4c7a372c9691cff9ac3c fb8e4e3de479b3bf1f4f13b4ed5507df1e80bd9250567b9d021b03339d6e7197
|
|
||||||
hash_to_ec 40a4e62800a99b7a26e0b507ffb29592e5bdba25284dc473048f24b27d25b40a 90ae131d29ee4a71cd764ab26f1ca4e6d09a40db98f8692b345c3a0e130dc860
|
|
||||||
hash_to_ec 1ddf35193cf52860bfe3e41060a7f44281241c6ae49cd541d24c1aca679b7501 3b4f50013895c522776ced456329c4e727de03575f6b99ae7d238a9f70862121
|
|
||||||
hash_to_ec 014e0fa8ce9d5df262b9a1765725fde354a855de8aef3fc23684e05dd1ba8d34 3857f57776a3cb68721bcb7f1533a5f9fb416a1dc8824d719399b63a142d24de
|
|
||||||
hash_to_ec 09987979b0e98d1d5355df8a8698b8f54d3a037d12745c0a4317fe519c3df9cc 32a181e2b754aeced214c73ac459c97d99e63317be3eb923344c64a396173bca
|
|
||||||
hash_to_ec 51e9e8ec4413e92dbaaba067824c32b018487a8d16412ed310507b4741e18eed 0356b209156b4993fd5d5630308298429a1b0021c19bedecb7719ac607cfa644
|
|
||||||
hash_to_ec 14d91313dfe46e353310e6a4a23ee15d7a4e1f431700a444be8520e6043d08d9 6f345f4018b5d178d9f61894d9f46ac09ff639483727b0d113943507cee88cfd
|
|
||||||
hash_to_ec 0d5af9ace87382acfffb9ab1a34b6e921881aa015d4f6d9c73171b2b0a97600d a8dbf36c85bebe6a7b3733e70cd3cd9ed0eb282ca470f344e5fcf9fe959f2e6e
|
|
||||||
hash_to_ec 996690caac7328b19d20ed28eb0003d675b1a9ff79055ab530e3bf170eb22a94 14340d7d935cffce74b8b2f325c9d92ce0238b51807ef2c1512935bb843194ce
|
|
||||||
hash_to_ec ad839c4b4c278c8ebe16ff137a558255a1f74646aa87c6cd99e994c7bb97ce8a d4f2da327ffded913b50577be0e583db2b237b5ca74da648e9b985c247073b76
|
|
||||||
hash_to_ec 26fc2eeeee983e1300d72362fdff42edf08038e4eee277a6e2dbd1bd8c9d6560 3468b8269728c2c0bfc2e53b1575415124798bc0f59b60ea2f14967fc0ca19ce
|
|
||||||
hash_to_ec db33cecaf4ee6f0ceba338cc5fabfb7462cd952a9c9007357ff3f0ca8336f8bc 0bab38f58686d0ff770f770a297971510bc83e2ff2dfead34823d1c4d67f11af
|
|
||||||
hash_to_ec a0ee84b3c646526fb8787d26dcd9b7fe9dc713c8a6c1a4ea640465a9f36a64df 4d7a638f6759d3ec45339cd1300e1239cca5f0f658ca3cd29bc9bdb32f44faf0
|
|
||||||
hash_to_ec 6a702e7899fcf3988e2b6b55654c22e54f43d3fa29de19177bdff5b2295fe27f 145d5748d6054fb586568e276f6925aef593a5b9c8249ad3dbef510af99b4307
|
|
||||||
hash_to_ec 30ce0fd4f1fac8b62d613b8ee4a66deef6eb7094bd8466531050b837460f6971 f3aa850d593ba7cef01389f7e1916e57617f1d75cd42f64ce8f5f272384b148c
|
|
||||||
hash_to_ec 3aa31d4ad7046ad13d83eb11c9a6e90eb8483a374a77a9a7b2a7cc0978fefa76 2fe0827dc080d9c1e7ec475a78aa7ae3c86d1a35f4c3f25f4a1f7299cacf018a
|
|
||||||
hash_to_ec 8562a5a91e763b98014523ebb6e49120979098f89c31df1fde9eb3a49a15b20f ae223bf85e2009a9daf5fd8a14685e2e1e625fc88818b2fd437dd7e109a48f59
|
|
||||||
hash_to_ec ccf9c313a47b8dbf7ce42c94b785818bc24134d95b6d22acc53c1ec2be29cf27 3e79fce6fe5aa14251b6560df4b76e811d7739eec097f27052c4403a283be71d
|
|
||||||
hash_to_ec d1e33cd6f8918618d5fb6d67ad8de939db8beaec4f115551eac64479b739b773 613fffcbe1bf48bb2d7bfd64fd97790a06025f8f2429edddb9ac145707847ecf
|
|
||||||
hash_to_ec 81eaeced34dd44e448d5dafa5715225e4956c90911c964a96ff7aa5b86b969bc 8f81177495d120a1357380164d677509b167f2958eb8b962b616c3951d426d8c
|
|
||||||
hash_to_ec 2bc001a29f8eab1c7377de69957ba365fb5bdaf9c2c220889709af920dfe27d3 9bcb3010038f366fa4c280eed6e914a23bfc402594d0b83d0e66730a465a565b
|
|
||||||
hash_to_ec 6feeb703c05e86c58d9fc5623f1af8657ecd1e75a14d18c4eedb642a8a393d16 6544628ba67ed0e14854961739c4d467fcf49d6361e39d32ea73dabeae51e6c3
|
|
||||||
hash_to_ec e8ff145a7c26897f2c1639edd333a5412f87752f110079f581ccdc87fcce208c d4b5a6e06069c7e012e32119f8eda08ff04a8dfa784e1cf1bced455a4d41d905
|
|
||||||
hash_to_ec 80488131dcb2018527908dbf8cdf4b823ef0806dc1d360f4da671004ef7ff74d 9984a79d9fd4f317768b442161116eef84e2ca49e938642b268fd64312d59a27
|
|
||||||
hash_to_ec d8c4ca60446849a784d1462aa26a3b93073ff6841cb2da3ef52ab9785b00b1fd da5ec1562e7de2382d35728312f4eea3608d4dba775c1c108de510e1ce97d059
|
|
||||||
hash_to_ec 68645728dfc6b9358dfb426493238ba38f24a2f46a3e89edb47d212549939cb7 d3253aa7235113dcc1b577d3bb80be34f528398815a653dbdbacbcbdfd5887a1
|
|
||||||
hash_to_ec 4e8eb97ba2d1046e1b42e67530a61441e31c84e5e5e448d8e8dbe75d104eaccb de94f73e83222aa0e39b559d4fef70387b0815b9b2f6beff5da67262d8f0eb3e
|
|
||||||
hash_to_ec 104ff03122ffdf59b22b8c0fe3d8f2ef67d02328e4d5181916d3d2a92f9a0bb7 1517ccf69c0328327e1cf581f16944ff66bc91c37e1cd68a99525415e00b7c9f
|
|
||||||
hash_to_ec 80f23aae7356ae9a2f9f7504495a731214d26f870fb7df68fdc00b233494156f 7aef046b0a70f84e8d239aa95e192b5a3fffa0fae5090c91273e8996beca9e38
|
|
||||||
hash_to_ec 2424b33235955a737ebddbf1c6c59cd8778af74da3bd3e658447666a2ab2f557 d19e2be8d482950fbdae429618da7a9daedb8c5944dea19cd1b6b274e792231b
|
|
||||||
hash_to_ec 0adc839d2b8f099e4341a4763b074c06318d6bcbd1ec558d20a9820c4a426463 cea5da12a84e5c20011726d9224a9930bec30f9571762dd7ca857b86bd37d056
|
|
||||||
hash_to_ec 46c84d53951f1ba23c46a23d5d96bf019c559aa5d2d79e4535cfcdb36f38ce25 2a913a01a6f7dd78a43cdd5354d1160d9a5f0d824c489a892c80eba798a77567
|
|
||||||
hash_to_ec 99bdaaf68555ccdc93d97c3a0fb4c126a1aa8b1202194a1a753401a6cae21055 1f645efe173577a092f2d847cc966e28ba3b36397fe84c96dfa4724ed4fcfdf9
|
|
||||||
hash_to_ec c540ff78f1e063ad26ffa69febb8818c9f2a325072c566091ad816e40fe39af4 de7a762262c91ab4beccc0713233cb91163aec43e34de0dbcfad0c431e8a9722
|
|
||||||
hash_to_ec de8b1ff8978cd5e02681521542b7b6c3c2f8f4602065059f83594809d04e3dda 290601e75207085bff3e016746e55a80310a76dea9ef566c24181079c76da11c
|
|
||||||
hash_to_ec d555994c8a022e52602d2a8bdd01fc1bfa6b9ab6734ff72a1bd5f937de4627f8 5f6794e874f48c4b362d0a24207374c2d274e28de86351afc6ddb95d8cc2fd62
|
|
||||||
hash_to_ec 19db72f703fe6f1b73f21b6ba133ae6b111ae8cc496d3aa32e02411e34c0d8d7 42f159f43d2d62b8cf8a47d5f1340c5cf070e9860fc60de647c55d50fe9f5607
|
|
||||||
hash_to_ec 23a87a258c2a5d1353aa2d5946f9e5749b92f85e3c58e1d177c3b6c3dcac809c e5685016f79d5e87d1fecb3e2a0fe64e4875f7accd2f6649d7f6b16317549cb1
|
|
||||||
hash_to_ec 43e1738d7d1b5b565f5fc78e81480f7edf9a4dc18f104fc4be95135b98931b17 650f5b682e45f2d0c5d5e8bcfd9e0cda7d9071b55ecbfaf5e3b59941cd7479f2
|
|
||||||
hash_to_ec a9d644de0804edf62dee613efa2547e510990a9b7a987ebe55ec74c23873a878 52ad329f88499a4f110e6a6cba1f820012d8db6ccb8f6495ab1e3eb5a24786e1
|
|
||||||
hash_to_ec 11f2b5d89a0350d7c8727becf0f4dd19bd90f8c94ff207132ab13282dd9b94e6 b798a47bb98dc2a8f99deaf64d27638e33a0d504c5d2fbee477a2bc9b89e2838
|
|
||||||
hash_to_ec 5e206e3190b3b715d125f1a11fff424fb33e36e534c99ddde2a3517068b7dcc4 2738e9571c96b2ddf93cb5f4a72b1ea78d3731d9555b830494513c0683c950ca
|
|
||||||
hash_to_ec efc3d65a43d4f10795c7265a76671348f80173e0f507c812f7ae76793b99c529 cf4434d18ce8167b51f117fe930860143c46e1739a8db1fba73b6b0de830d707
|
|
||||||
hash_to_ec 81f00469788aad6631cf75b585ae06d43ec81c20479925a2009afac9687dff60 c335b5889b36ba4b4175bb0d986807e8eedb6f6b7329b70b922e2ab729c4202a
|
|
||||||
hash_to_ec 9ef5ff329b525ee8f5c3ac38e1dba7cb19985617341d356707c67ff273aed02d bef9f9e051ba0e24d1fdf72099cf43ecdd250d047fb329855b5372d5c422db9e
|
|
||||||
hash_to_ec 3fa1401bd63132cf8b385c0fa65f0715ba1fe6161e41d59f8033ae2b22f63fa1 8289a1cb3c2dae48879bb8913fafe2d196cc2fdab5f2a77607910efd33eae6df
|
|
||||||
hash_to_ec 6559836fd0081fa38a3f8d8408b564e5698b9797cf5e15f7f12a7d2c84511989 28d405a6687d2ecc90c1c66bf0454d58f3fa38835743075e1db58c658e15a104
|
|
||||||
hash_to_ec 8e0882d45f0e4c2fb2839d3be86ff699d4b2242f5b25ac5a3c2f65297c7d2032 2771fdcf9135a62007adb5f0004d8222f0e42f819c81710aa4dc3ab2042bebf3
|
|
||||||
hash_to_ec 1d91dc4dd9bd82646029d13aca1af96830c1d8a0400ddebeb14b00c93501c039 7792c62e897f32cbc9c4229f0d28f7882ceeae120329a1cd35f76a75ac704e93
|
|
||||||
hash_to_ec 09527f9052acbbdd7676cbbd9534780865f04a27aaadad2b7d4f1dac68883cf0 b934220cde1327f2dc6af67bcb4124bf424d5084ef4da945e4daad1717cd0bb8
|
|
||||||
hash_to_ec 2362e1abe73e64cdd2ca7f6c5ea9f467213747dd3f2b7c6e5df9cb21e03307d7 676b7122b96564358bbaaf77e3a5a4db1767e4f9a50f6ddd1c69df4566755af9
|
|
||||||
hash_to_ec 26c2dd2356e9b6c68a415b25f91d18614dc8500c66f346d28489da543ee75a94 0f4fd7086acd68eb7c9fa2410e2ecf18e34654eb44e979bc03ce436e992d5feb
|
|
||||||
hash_to_ec 422dc0a09d6a45a8e0b563eeb6a5ee84b08abd3a8cb34ff93f77ba3b163f4042 631f1b412ff5a0fccbe53a02b4a3deaa93a0418ed9874df401eb698ef75d7441
|
|
||||||
hash_to_ec ceecdf46f57ef3f36ff30a1a3579b609340282d1b26ab5ddef2f53514e91bab1 9bc6f981fe98d14a2fc5b01a8134b6d35e123ec9ab8a3f303e0a5abb28150e2e
|
|
||||||
hash_to_ec 024a9e6e0d73f28aa6207fb1e02ce86d444d2d46f8211e8aaab54f459db91a5a 5fb0c1d2c3b30f399102104ea1874099fa83110b3d9c1fcfffb2981c98bf8cdf
|
|
||||||
hash_to_ec 5b8e45e269c9ccac4c68e532a72b29346d218f4606f37a14064826a62050e3a8 c7be46a871b77fc05ce891d24bd6bd54d9775b7ef573c6bc2d92b67f3604c1d1
|
|
||||||
hash_to_ec 9a6593a385c266389eef14237874b97bdcd1823c3199311667d4853c2d12aa81 9f55ee9d94102d2b9c5670f30586cf9823bf205b4d4fe088c323e87c4e10f26f
|
|
||||||
hash_to_ec 27377e2811598c3569b92990865d39b72c7a5533e1be30f77330863187c11875 abd82bc726f2710a8b87e4c1cf5a069f0ae800de614468d3ff35639983020197
|
|
||||||
hash_to_ec 7cacfaa135fb7d568b8dce8ea9136498b1b28c6d1020af45d376288d78d411f0 229fccd49744c0692508af329224553d21561ee6062b2b8a21f080f73da5bd97
|
|
||||||
hash_to_ec 52abd90a5542d6496b8dec9567b020f30058e29458d64f2d4f3ad6f3bfc1a5a0 874e82ced7cf77577b3374087fb08a2300b7f403de628310c26bdb3be869d309
|
|
||||||
hash_to_ec 5c8eebe9d12309187afa8d0d5191de3fdb84e5a05485d7cd62e8804ce7fdc0bc 12b7537643488aa8b9dcc4bae040cd491f8b466163b7988157b0502fb6c9177f
|
|
||||||
hash_to_ec 6ca3dd5c7a21a6bf65d6eefbe20a66e9b1d6b64196344be0c075f47aea48e3aa 5e1d0705ee24675238293b73ab1d98359119d4b328275be2460cc6ee4d19cc88
|
|
||||||
hash_to_ec d7e6cd0d39b4308c2a5ee547c4569c8bb3887e49cedece62d218d7c3c5277797 793dc4397112dfd9a8f4e061f457eb6d6fbb1d7a58c40bad5f16002c64914186
|
|
||||||
hash_to_ec 9cb6de8ba967cca0f0f861c6e20546f8958446595c01c28dae7ba6cfa09d6b14 ba1a2f7502b58fee3499c20e35fa01bb932e7a7c4a925dc04fbf5d90f33cfb5e
|
|
||||||
hash_to_ec 8ef9c7366733a1edcd116238cdbd177d61222d5c3e05b30ef6b85014cbcb6b79 8fc89664722947164ac9b77086aed319897612068f56ecd57f47029f14671603
|
|
||||||
hash_to_ec 7f317a34e4fb7de9f69cb107ffc0e57fd9f5c85b85ccb5319d05cebfc169924a 4b71c42339c73db7d710cd63f374d478a6c13bdc352cff40e967282268965ba7
|
|
||||||
hash_to_ec 15beef8d9687b92918a903b01d594859db4e7128263c8db0cae9d423ff962c1e cd75e6323952f6ac88f138f391b69f38c46d70b7eda61f9e431725b6f1d514a5
|
|
||||||
hash_to_ec 7a1c04c9af8fc6649833fe81e96f0199fcfe94959256cbe1490075fc5be0904e 0368270cd979439ae0a9552a5d6c9f959e4247fcf920d9e071464582e79c04b1
|
|
||||||
hash_to_ec c854c583d338615f85f69061e0fa9c9d7c5bbbfe562e8774fef3be556fe8bb63 061620171d7320f64bee98414ff7200a1f481521d202fb281cab06be73b80402
|
|
||||||
hash_to_ec 0fb8af5aba05ad2503edf1cfad5a451da088e7e974772057cd991a4e0601a3eb d3cbc20384a4420143fcce2cb763b0c15bec4f3267d1bdad3c34c1ee6b790f5e
|
|
||||||
hash_to_ec 9a251cf59e84a9da5630642f9671c732440caa8fcf4c92446a7e5f5ef99da46c 9b9679086a433f2077f40bcd4c7545fb5cc87e7dbb8bba468d53cb04a74361a0
|
|
||||||
hash_to_ec 8c632e357cef00e0911eb566f8cc809136b3f5ac1e82d183e4d645cef89fa155 5e06b0f4f278fa1ccb5431866e0b35171cdb814e2e82b9189ce01d8d8a1b2408
|
|
||||||
hash_to_ec 4aa4c31463475086a5d96b3ff550340567ab3b4a86fa3f01cfe9be18bc4dcb54 76a2916cfc093f27992e1f07b50f431d61d58e255507e208cd29ea4d3bc56623
|
|
||||||
hash_to_ec 1d33d9aadb949346e3c78d065a0f5262374524f4cb97a7390c8cdaede7ca6578 9ad2f757f499359903031adea6126c577469c4e834a2959e3ac08ee74b13783c
|
|
||||||
hash_to_ec d9217b9a070df20c4d2f0db42ff0bb36bfba9f51b0b6df8fdfe150405dce4934 65a843c522b4b8ec081a696a0d2dd8dfdfea45db201de7a5889a1446c6dff8c7
|
|
||||||
hash_to_ec b665b2ca8a285e44ba84e785533b56496a5319730dbb95bc14d3bdfece7544dc 8a804cd13457497b0a29eeca2cecfaa858766ec1d270a0e0c6785b43fd49b824
|
|
||||||
hash_to_ec 43b5cbcc21b3404bca97fa9a661940fe64d40f3ca569310e50b1bb0173c4d5ee 6c12fffb540d536060bb8b96cf635c1b2cbaa4d875a8d2fb0bf79a690363df19
|
|
||||||
hash_to_ec 11c58f20562c00dec5bb4456be07cd98186837e9af38d50d45f5e7b6f0f9000d cee76b567586f66dadd38c01213bfc1a17d38e96a495efb4c26063dc498ba209
|
|
||||||
hash_to_ec b069a980b51d8e030262db0b30069e660f4a3f6f8075d1790c153ba12b879f8b 262391b00bdee71d1d827b2cfe50b46c29e265934dc91959bd369aca0cc6444e
|
|
||||||
hash_to_ec 75274bfd79bf33eb2f9ab046d34528af9a71811e7e3d55c20eb049c81ac692d8 cb93c850e36896fe6626e97c53652af6736ec3ba0641c7765d0cca2bad2352de
|
|
||||||
hash_to_ec 5cdb6a24d9736a00f197d9707949fedc5405f367744fe8c83b7cff650302b589 8b4ac03123fab9275dcf340345a1b11fba48ef106d410ba2e0e6f6457037a419
|
|
||||||
hash_to_ec 07fdc85f809f95a07b59b084402bf91c512ebbe05c7657d6ba27a9e7e121e3e2 61182b3def063630e11de648a278032bcb75949f3a24ef5a133da87830ae5c4e
|
|
||||||
hash_to_ec a4188ca634cbb796f9927822e343d7b267e0a609c1a0ffa4dcf3726b9ffcc8a2 a911e4899fda28fd6337d708d34553ac5e810ee4938f6f7d9d6e521cab069edb
|
|
||||||
hash_to_ec 3c128ec5c955ea189a5789df2c892e94193a534a9d5801b8f75df870bc492a69 59eef5ee9df0f681df5b5c67ead1f06b059a8a843837b67f20cce15779608170
|
|
||||||
hash_to_ec 51a4cc7ec4a14a98c0731e9de7f3ce0779123222d95455e940f2014a23729ec8 105863ccda076af7290d1bf9ec828651dc5811159839044d23f1c3e31a11c5e2
|
|
||||||
hash_to_ec 1b901a31acbb7807c3309facdc7d04bc3b5a4aa714e6e346bd1c6ad4634e6534 01b3c0000b6c6b471c67c6ab3f9c7a500beaea5edb5c8f2b34df91b69ff67f21
|
|
||||||
hash_to_ec d2f2c8d79cfa2e7cb2db80568ba62ca0576741acfbe5e2baa0d9b3c424a7c84d 7df9d9088022bd1ce6814d6f8051eef27a650ee38e789b184da2691efd27139d
|
|
||||||
hash_to_ec 04dcb7644fdfc12d8e34d6e57d7769db939b4a149ed2b81aa51a74ee90babe19 6cff0ab2dd3b32ba1bd1a78e3661722f3f10003a01ce83e430970557decedb2c
|
|
||||||
hash_to_ec 222798c6841eeaa07e7b7e29686942d7c7f9afc38d09360c8e1f52f2b7debd12 133e3a04ec82aa9b8dbbec18cadbafff446d1270bf7c6f3f97ddd3906dae2468
|
|
||||||
hash_to_ec 4f7277c3ef247a0689b486ad965f969c433fc63e95d7310e789c4708418ccabc 7e0f2c984dd3cffb35458938c95fe92acf2e697aed060b0e3377c7a07e53c494
|
|
||||||
hash_to_ec 359b4d6709413243ae2c5409ea02714a9f8961bbbb64a91e81daf01e18c981bf eab69af2cb7f113ad6a27035c0399853d10bd0b99291fad37794d100f7530431
|
|
||||||
hash_to_ec 6cea3c6a9eb38f60329537170aa4db8dbb869af2040061e53b10c267daf6568c da9a97f4fa96bd05dade5e2704a6a633ba4dbe5080a1e831cda888e9d4f86615
|
|
||||||
hash_to_ec 3dddecb954ef0209bcf61fd5b46b6c94f2384ef281c48a20ffee74f90788172d af9899c31f944617af54712f93d1a2b4944e48867f480d0d1aec61f3b713e32d
|
|
||||||
hash_to_ec 9605247462f50bdf7ff57fe966abbefe8b6efa0b65b5116252f0ec723717013f fc8f10904d42a74e09310ccf63db31a90f1dab88b278f15e3364a2356810f7e9
|
|
||||||
hash_to_ec a005143c4d299933f866db41d0a0b8c67264f5d4ea840dd243cb10c3526bc077 928df1fe9404ffa9c1f4a1c8b2d43ab9b81c5615c8330d2dc2074ac66d4d5200
|
|
||||||
hash_to_ec f45ce88065c34a163f8e77b6fb583502ed0eb1f490f63f76065a9d97e214e3a9 41bd6784270af4154f2f24f118617e2d7f5b7771a409f08b0f2b7bbcb5e3d666
|
|
||||||
hash_to_ec 7b40ac30ed02b12ff592a5479c80cf5a7673abfdd4dd38810e40e63275bc2eed 6c6bf5961d83851c9728801093d9af04e5a693bc6cbad237b9ac4b0ed580a771
|
|
||||||
hash_to_ec 9f985005794d3052a63361413a9820d2ce903198d6d5195b3f20a68f146c6d5c 88bcac53ba5b1c5b44730a24b4cc2cd782298fc70dc9d777b577a2b33b256449
|
|
||||||
hash_to_ec 31b8e37d01fd5669de4ebf78889d749bc44ffe997186ace56f1fb3e60b8742d2 776366b44170efb130a5045597db5675c6c0b56f3def84863c6b6358aa8dcf40
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-io"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Serialization functions, as within the Monero protocol"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/io"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = ["std-shims/std"]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Monero IO
|
|
||||||
|
|
||||||
Serialization functions, as within the Monero protocol.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use core::fmt::Debug;
|
|
||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use curve25519_dalek::{
|
|
||||||
scalar::Scalar,
|
|
||||||
edwards::{EdwardsPoint, CompressedEdwardsY},
|
|
||||||
};
|
|
||||||
|
|
||||||
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
|
||||||
|
|
||||||
mod sealed {
|
|
||||||
use core::fmt::Debug;
|
|
||||||
|
|
||||||
/// A trait for a number readable/writable as a VarInt.
|
|
||||||
///
|
|
||||||
/// This is sealed to prevent unintended implementations.
|
|
||||||
pub trait VarInt: TryInto<u64, Error: Debug> + TryFrom<u64, Error: Debug> + Copy {
|
|
||||||
const BITS: usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VarInt for u8 {
|
|
||||||
const BITS: usize = 8;
|
|
||||||
}
|
|
||||||
impl VarInt for u32 {
|
|
||||||
const BITS: usize = 32;
|
|
||||||
}
|
|
||||||
impl VarInt for u64 {
|
|
||||||
const BITS: usize = 64;
|
|
||||||
}
|
|
||||||
// Don't compile for platforms where `usize` exceeds `u64`, preventing various possible runtime
|
|
||||||
// exceptions
|
|
||||||
const _NO_128_BIT_PLATFORMS: [(); (u64::BITS - usize::BITS) as usize] =
|
|
||||||
[(); (u64::BITS - usize::BITS) as usize];
|
|
||||||
impl VarInt for usize {
|
|
||||||
const BITS: usize = core::mem::size_of::<usize>() * 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The amount of bytes this number will take when serialized as a VarInt.
|
|
||||||
///
|
|
||||||
/// This function will panic if the VarInt exceeds u64::MAX.
|
|
||||||
pub fn varint_len<V: sealed::VarInt>(varint: V) -> usize {
|
|
||||||
let varint_u64: u64 = varint.try_into().expect("varint exceeded u64");
|
|
||||||
((usize::try_from(u64::BITS - varint_u64.leading_zeros())
|
|
||||||
.expect("64 > usize::MAX")
|
|
||||||
.saturating_sub(1)) /
|
|
||||||
7) +
|
|
||||||
1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a byte.
|
|
||||||
///
|
|
||||||
/// This is used as a building block within generic functions.
|
|
||||||
pub fn write_byte<W: Write>(byte: &u8, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&[*byte])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a number, VarInt-encoded.
|
|
||||||
///
|
|
||||||
/// This will panic if the VarInt exceeds u64::MAX.
|
|
||||||
pub fn write_varint<W: Write, U: sealed::VarInt>(varint: &U, w: &mut W) -> io::Result<()> {
|
|
||||||
let mut varint: u64 = (*varint).try_into().expect("varint exceeded u64");
|
|
||||||
while {
|
|
||||||
let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK))
|
|
||||||
.expect("& eight_bit_mask left more than 8 bits set");
|
|
||||||
varint >>= 7;
|
|
||||||
if varint != 0 {
|
|
||||||
b |= VARINT_CONTINUATION_MASK;
|
|
||||||
}
|
|
||||||
write_byte(&b, w)?;
|
|
||||||
varint != 0
|
|
||||||
} {}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a scalar.
|
|
||||||
pub fn write_scalar<W: Write>(scalar: &Scalar, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&scalar.to_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a point.
|
|
||||||
pub fn write_point<W: Write>(point: &EdwardsPoint, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&point.compress().to_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a list of elements, without length-prefixing.
|
|
||||||
pub fn write_raw_vec<T, W: Write, F: Fn(&T, &mut W) -> io::Result<()>>(
|
|
||||||
f: F,
|
|
||||||
values: &[T],
|
|
||||||
w: &mut W,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
for value in values {
|
|
||||||
f(value, w)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a list of elements, with length-prefixing.
|
|
||||||
pub fn write_vec<T, W: Write, F: Fn(&T, &mut W) -> io::Result<()>>(
|
|
||||||
f: F,
|
|
||||||
values: &[T],
|
|
||||||
w: &mut W,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
write_varint(&values.len(), w)?;
|
|
||||||
write_raw_vec(f, values, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a constant amount of bytes.
|
|
||||||
pub fn read_bytes<R: Read, const N: usize>(r: &mut R) -> io::Result<[u8; N]> {
|
|
||||||
let mut res = [0; N];
|
|
||||||
r.read_exact(&mut res)?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a single byte.
|
|
||||||
pub fn read_byte<R: Read>(r: &mut R) -> io::Result<u8> {
|
|
||||||
Ok(read_bytes::<_, 1>(r)?[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a u16, little-endian encoded.
|
|
||||||
pub fn read_u16<R: Read>(r: &mut R) -> io::Result<u16> {
|
|
||||||
read_bytes(r).map(u16::from_le_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a u32, little-endian encoded.
|
|
||||||
pub fn read_u32<R: Read>(r: &mut R) -> io::Result<u32> {
|
|
||||||
read_bytes(r).map(u32::from_le_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a u64, little-endian encoded.
|
|
||||||
pub fn read_u64<R: Read>(r: &mut R) -> io::Result<u64> {
|
|
||||||
read_bytes(r).map(u64::from_le_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a canonically-encoded VarInt.
|
|
||||||
pub fn read_varint<R: Read, U: sealed::VarInt>(r: &mut R) -> io::Result<U> {
|
|
||||||
let mut bits = 0;
|
|
||||||
let mut res = 0;
|
|
||||||
while {
|
|
||||||
let b = read_byte(r)?;
|
|
||||||
if (bits != 0) && (b == 0) {
|
|
||||||
Err(io::Error::other("non-canonical varint"))?;
|
|
||||||
}
|
|
||||||
if ((bits + 7) >= U::BITS) && (b >= (1 << (U::BITS - bits))) {
|
|
||||||
Err(io::Error::other("varint overflow"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
res += u64::from(b & (!VARINT_CONTINUATION_MASK)) << bits;
|
|
||||||
bits += 7;
|
|
||||||
b & VARINT_CONTINUATION_MASK == VARINT_CONTINUATION_MASK
|
|
||||||
} {}
|
|
||||||
res.try_into().map_err(|_| io::Error::other("VarInt does not fit into integer type"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a canonically-encoded scalar.
|
|
||||||
///
|
|
||||||
/// Some scalars within the Monero protocol are not enforced to be canonically encoded. For such
|
|
||||||
/// scalars, they should be represented as `[u8; 32]` and later converted to scalars as relevant.
|
|
||||||
pub fn read_scalar<R: Read>(r: &mut R) -> io::Result<Scalar> {
|
|
||||||
Option::from(Scalar::from_canonical_bytes(read_bytes(r)?))
|
|
||||||
.ok_or_else(|| io::Error::other("unreduced scalar"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decompress a canonically-encoded Ed25519 point.
|
|
||||||
///
|
|
||||||
/// Ed25519 is of order `8 * l`. This function ensures each of those `8 * l` points have a singular
|
|
||||||
/// encoding by checking points aren't encoded with an unreduced field element, and aren't negative
|
|
||||||
/// when the negative is equivalent (0 == -0).
|
|
||||||
///
|
|
||||||
/// Since this decodes an Ed25519 point, it does not check the point is in the prime-order
|
|
||||||
/// subgroup. Torsioned points do have a canonical encoding, and only aren't canonical when
|
|
||||||
/// considered in relation to the prime-order subgroup.
|
|
||||||
pub fn decompress_point(bytes: [u8; 32]) -> Option<EdwardsPoint> {
|
|
||||||
CompressedEdwardsY(bytes)
|
|
||||||
.decompress()
|
|
||||||
// Ban points which are either unreduced or -0
|
|
||||||
.filter(|point| point.compress().to_bytes() == bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a canonically-encoded Ed25519 point.
|
|
||||||
///
|
|
||||||
/// This internally calls `decompress_point` and has the same definition of canonicity. This
|
|
||||||
/// function does not check the resulting point is within the prime-order subgroup.
|
|
||||||
pub fn read_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
|
||||||
let bytes = read_bytes(r)?;
|
|
||||||
decompress_point(bytes).ok_or_else(|| io::Error::other("invalid point"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a canonically-encoded Ed25519 point, within the prime-order subgroup.
|
|
||||||
pub fn read_torsion_free_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
|
||||||
read_point(r)
|
|
||||||
.ok()
|
|
||||||
.filter(EdwardsPoint::is_torsion_free)
|
|
||||||
.ok_or_else(|| io::Error::other("invalid point"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a variable-length list of elements, without length-prefixing.
|
|
||||||
pub fn read_raw_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
|
||||||
f: F,
|
|
||||||
len: usize,
|
|
||||||
r: &mut R,
|
|
||||||
) -> io::Result<Vec<T>> {
|
|
||||||
let mut res = vec![];
|
|
||||||
for _ in 0 .. len {
|
|
||||||
res.push(f(r)?);
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a constant-length list of elements.
|
|
||||||
pub fn read_array<R: Read, T: Debug, F: Fn(&mut R) -> io::Result<T>, const N: usize>(
|
|
||||||
f: F,
|
|
||||||
r: &mut R,
|
|
||||||
) -> io::Result<[T; N]> {
|
|
||||||
read_raw_vec(f, N, r).map(|vec| {
|
|
||||||
vec.try_into().expect(
|
|
||||||
"read vector of specific length yet couldn't transform to an array of the same length",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a length-prefixed variable-length list of elements.
|
|
||||||
///
|
|
||||||
/// An optional bound on the length of the result may be provided. If `None`, the returned `Vec`
|
|
||||||
/// will be of the length read off the reader, if successfully read. If `Some(_)`, an error will be
|
|
||||||
/// raised if the length read off the read is greater than the bound.
|
|
||||||
pub fn read_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
|
||||||
f: F,
|
|
||||||
length_bound: Option<usize>,
|
|
||||||
r: &mut R,
|
|
||||||
) -> io::Result<Vec<T>> {
|
|
||||||
let declared_length: usize = read_varint(r)?;
|
|
||||||
if let Some(length_bound) = length_bound {
|
|
||||||
if declared_length > length_bound {
|
|
||||||
Err(io::Error::other("vector exceeds bound on length"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
read_raw_vec(f, declared_length, r)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-primitives"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Primitives for the Monero protocol"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/primitives"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
# Cryptographic dependencies
|
|
||||||
sha3 = { version = "0.10", default-features = false }
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
# Other Monero dependencies
|
|
||||||
monero-io = { path = "../io", version = "0.1", default-features = false }
|
|
||||||
monero-generators = { path = "../generators", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
|
|
||||||
"sha3/std",
|
|
||||||
|
|
||||||
"monero-generators/std",
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Monero Primitives
|
|
||||||
|
|
||||||
Primitive structures and functions for the Monero protocol.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use std_shims::{io, vec::Vec};
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use std_shims::sync::LazyLock;
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use sha3::{Digest, Keccak256};
|
|
||||||
use curve25519_dalek::{
|
|
||||||
constants::ED25519_BASEPOINT_POINT,
|
|
||||||
traits::VartimePrecomputedMultiscalarMul,
|
|
||||||
scalar::Scalar,
|
|
||||||
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_io::*;
|
|
||||||
use monero_generators::H;
|
|
||||||
|
|
||||||
mod unreduced_scalar;
|
|
||||||
pub use unreduced_scalar::UnreducedScalar;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
// On std, we cache some variables in statics.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
static INV_EIGHT_CELL: LazyLock<Scalar> = LazyLock::new(|| Scalar::from(8u8).invert());
|
|
||||||
/// The inverse of 8 over l, the prime factor of the order of Ed25519.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn INV_EIGHT() -> Scalar {
|
|
||||||
*INV_EIGHT_CELL
|
|
||||||
}
|
|
||||||
// In no-std environments, we prefer the reduced memory use and calculate it ad-hoc.
|
|
||||||
/// The inverse of 8 over l, the prime factor of the order of Ed25519.
|
|
||||||
#[cfg(not(feature = "std"))]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn INV_EIGHT() -> Scalar {
|
|
||||||
Scalar::from(8u8).invert()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
static G_PRECOMP_CELL: LazyLock<VartimeEdwardsPrecomputation> =
|
|
||||||
LazyLock::new(|| VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT]));
|
|
||||||
/// A cached (if std) pre-computation of the Ed25519 generator, G.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn G_PRECOMP() -> &'static VartimeEdwardsPrecomputation {
|
|
||||||
&G_PRECOMP_CELL
|
|
||||||
}
|
|
||||||
/// A cached (if std) pre-computation of the Ed25519 generator, G.
|
|
||||||
#[cfg(not(feature = "std"))]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn G_PRECOMP() -> VartimeEdwardsPrecomputation {
|
|
||||||
VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Keccak-256 hash function.
|
|
||||||
pub fn keccak256(data: impl AsRef<[u8]>) -> [u8; 32] {
|
|
||||||
Keccak256::digest(data.as_ref()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hash the provided data to a scalar via keccak256(data) % l.
|
|
||||||
///
|
|
||||||
/// This function panics if it finds the Keccak-256 preimage for [0; 32].
|
|
||||||
pub fn keccak256_to_scalar(data: impl AsRef<[u8]>) -> Scalar {
|
|
||||||
let scalar = Scalar::from_bytes_mod_order(keccak256(data.as_ref()));
|
|
||||||
// Monero will explicitly error in this case
|
|
||||||
// This library acknowledges its practical impossibility of it occurring, and doesn't bother to
|
|
||||||
// code in logic to handle it. That said, if it ever occurs, something must happen in order to
|
|
||||||
// not generate/verify a proof we believe to be valid when it isn't
|
|
||||||
assert!(
|
|
||||||
scalar != Scalar::ZERO,
|
|
||||||
"keccak256(preimage) \\cong 0 \\mod l! Preimage: {:?}",
|
|
||||||
data.as_ref()
|
|
||||||
);
|
|
||||||
scalar
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transparent structure representing a Pedersen commitment's contents.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct Commitment {
|
|
||||||
/// The mask for this commitment.
|
|
||||||
pub mask: Scalar,
|
|
||||||
/// The amount committed to by this commitment.
|
|
||||||
pub amount: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for Commitment {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt.debug_struct("Commitment").field("amount", &self.amount).finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Commitment {
|
|
||||||
/// A commitment to zero, defined with a mask of 1 (as to not be the identity).
|
|
||||||
pub fn zero() -> Commitment {
|
|
||||||
Commitment { mask: Scalar::ONE, amount: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new Commitment.
|
|
||||||
pub fn new(mask: Scalar, amount: u64) -> Commitment {
|
|
||||||
Commitment { mask, amount }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the Pedersen commitment, as a point, from this transparent structure.
|
|
||||||
pub fn calculate(&self) -> EdwardsPoint {
|
|
||||||
EdwardsPoint::vartime_double_scalar_mul_basepoint(&Scalar::from(self.amount), &H, &self.mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Commitment.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.mask.to_bytes())?;
|
|
||||||
w.write_all(&self.amount.to_le_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the Commitment to a `Vec<u8>`.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(32 + 8);
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Commitment.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn read<R: io::Read>(r: &mut R) -> io::Result<Commitment> {
|
|
||||||
Ok(Commitment::new(read_scalar(r)?, read_u64(r)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decoy data, as used for producing Monero's ring signatures.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct Decoys {
|
|
||||||
offsets: Vec<u64>,
|
|
||||||
signer_index: u8,
|
|
||||||
ring: Vec<[EdwardsPoint; 2]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for Decoys {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt
|
|
||||||
.debug_struct("Decoys")
|
|
||||||
.field("offsets", &self.offsets)
|
|
||||||
.field("ring", &self.ring)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::len_without_is_empty)]
|
|
||||||
impl Decoys {
|
|
||||||
/// Create a new instance of decoy data.
|
|
||||||
///
|
|
||||||
/// `offsets` are the positions of each ring member within the Monero blockchain, offset from the
|
|
||||||
/// prior member's position (with the initial ring member offset from 0).
|
|
||||||
pub fn new(offsets: Vec<u64>, signer_index: u8, ring: Vec<[EdwardsPoint; 2]>) -> Option<Self> {
|
|
||||||
if (offsets.len() > usize::from(u8::MAX)) ||
|
|
||||||
(offsets.len() != ring.len()) ||
|
|
||||||
(usize::from(signer_index) >= ring.len())
|
|
||||||
{
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
// Check these offsets form representable positions
|
|
||||||
if offsets.iter().copied().try_fold(0, u64::checked_add).is_none() {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
Some(Decoys { offsets, signer_index, ring })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The length of the ring.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.offsets.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The positions of the ring members within the Monero blockchain, as their offsets.
|
|
||||||
///
|
|
||||||
/// The list is formatted as the position of the first ring member, then the offset from each
|
|
||||||
/// ring member to its prior.
|
|
||||||
pub fn offsets(&self) -> &[u64] {
|
|
||||||
&self.offsets
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The positions of the ring members within the Monero blockchain.
|
|
||||||
pub fn positions(&self) -> Vec<u64> {
|
|
||||||
let mut res = Vec::with_capacity(self.len());
|
|
||||||
res.push(self.offsets[0]);
|
|
||||||
for m in 1 .. self.len() {
|
|
||||||
res.push(res[m - 1] + self.offsets[m]);
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The index of the signer within the ring.
|
|
||||||
pub fn signer_index(&self) -> u8 {
|
|
||||||
self.signer_index
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The ring.
|
|
||||||
pub fn ring(&self) -> &[[EdwardsPoint; 2]] {
|
|
||||||
&self.ring
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The [key, commitment] pair of the signer.
|
|
||||||
pub fn signer_ring_members(&self) -> [EdwardsPoint; 2] {
|
|
||||||
self.ring[usize::from(self.signer_index)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Decoys.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> {
|
|
||||||
write_vec(write_varint, &self.offsets, w)?;
|
|
||||||
w.write_all(&[self.signer_index])?;
|
|
||||||
write_raw_vec(
|
|
||||||
|pair, w| {
|
|
||||||
write_point(&pair[0], w)?;
|
|
||||||
write_point(&pair[1], w)
|
|
||||||
},
|
|
||||||
&self.ring,
|
|
||||||
w,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the Decoys to a `Vec<u8>`.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res =
|
|
||||||
Vec::with_capacity((1 + (2 * self.offsets.len())) + 1 + 1 + (self.ring.len() * 64));
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a set of Decoys.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn read(r: &mut impl io::Read) -> io::Result<Decoys> {
|
|
||||||
let offsets = read_vec(read_varint, None, r)?;
|
|
||||||
let len = offsets.len();
|
|
||||||
Decoys::new(
|
|
||||||
offsets,
|
|
||||||
read_byte(r)?,
|
|
||||||
read_raw_vec(|r| Ok([read_point(r)?, read_point(r)?]), len, r)?,
|
|
||||||
)
|
|
||||||
.ok_or_else(|| io::Error::other("invalid Decoys"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
use curve25519_dalek::scalar::Scalar;
|
|
||||||
|
|
||||||
use crate::UnreducedScalar;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn recover_scalars() {
|
|
||||||
let test_recover = |stored: &str, recovered: &str| {
|
|
||||||
let stored = UnreducedScalar(hex::decode(stored).unwrap().try_into().unwrap());
|
|
||||||
let recovered =
|
|
||||||
Scalar::from_canonical_bytes(hex::decode(recovered).unwrap().try_into().unwrap()).unwrap();
|
|
||||||
assert_eq!(stored.ref10_slide_scalar_vartime(), recovered);
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://www.moneroinflation.com/static/data_py/report_scalars_df.pdf
|
|
||||||
// Table 4.
|
|
||||||
test_recover(
|
|
||||||
"cb2be144948166d0a9edb831ea586da0c376efa217871505ad77f6ff80f203f8",
|
|
||||||
"b8ffd6a1aee47828808ab0d4c8524cb5c376efa217871505ad77f6ff80f20308",
|
|
||||||
);
|
|
||||||
test_recover(
|
|
||||||
"343d3df8a1051c15a400649c423dc4ed58bef49c50caef6ca4a618b80dee22f4",
|
|
||||||
"21113355bc682e6d7a9d5b3f2137a30259bef49c50caef6ca4a618b80dee2204",
|
|
||||||
);
|
|
||||||
test_recover(
|
|
||||||
"c14f75d612800ca2c1dcfa387a42c9cc086c005bc94b18d204dd61342418eba7",
|
|
||||||
"4f473804b1d27ab2c789c80ab21d034a096c005bc94b18d204dd61342418eb07",
|
|
||||||
);
|
|
||||||
test_recover(
|
|
||||||
"000102030405060708090a0b0c0d0e0f826c4f6e2329a31bc5bc320af0b2bcbb",
|
|
||||||
"a124cfd387f461bf3719e03965ee6877826c4f6e2329a31bc5bc320af0b2bc0b",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
use core::cmp::Ordering;
|
|
||||||
use std_shims::{
|
|
||||||
sync::LazyLock,
|
|
||||||
io::{self, *},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::scalar::Scalar;
|
|
||||||
|
|
||||||
use monero_io::*;
|
|
||||||
|
|
||||||
// Precomputed scalars used to recover an incorrectly reduced scalar.
|
|
||||||
static PRECOMPUTED_SCALARS: LazyLock<[Scalar; 8]> = LazyLock::new(|| {
|
|
||||||
let mut precomputed_scalars = [Scalar::ONE; 8];
|
|
||||||
for (i, scalar) in precomputed_scalars.iter_mut().enumerate().skip(1) {
|
|
||||||
*scalar =
|
|
||||||
Scalar::from(u64::try_from((i * 2) + 1).expect("enumerating more than u64::MAX / 2 items"));
|
|
||||||
}
|
|
||||||
precomputed_scalars
|
|
||||||
});
|
|
||||||
|
|
||||||
/// An unreduced scalar.
|
|
||||||
///
|
|
||||||
/// While most of modern Monero enforces scalars be reduced, certain legacy parts of the code did
|
|
||||||
/// not. These section can generally simply be read as a scalar/reduced into a scalar when the time
|
|
||||||
/// comes, yet a couple have non-standard reductions performed.
|
|
||||||
///
|
|
||||||
/// This struct delays scalar conversions and offers the non-standard reduction.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct UnreducedScalar(pub [u8; 32]);
|
|
||||||
|
|
||||||
impl UnreducedScalar {
|
|
||||||
/// Write an UnreducedScalar.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an UnreducedScalar.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<UnreducedScalar> {
|
|
||||||
Ok(UnreducedScalar(read_bytes(r)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_bits(&self) -> [u8; 256] {
|
|
||||||
let mut bits = [0; 256];
|
|
||||||
for (i, bit) in bits.iter_mut().enumerate() {
|
|
||||||
*bit = core::hint::black_box(1 & (self.0[i / 8] >> (i % 8)))
|
|
||||||
}
|
|
||||||
|
|
||||||
bits
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computes the non-adjacent form of this scalar with width 5.
|
|
||||||
//
|
|
||||||
// This matches Monero's `slide` function and intentionally gives incorrect outputs under
|
|
||||||
// certain conditions in order to match Monero.
|
|
||||||
//
|
|
||||||
// This function does not execute in constant time and must only be used with public data.
|
|
||||||
fn non_adjacent_form(&self) -> [i8; 256] {
|
|
||||||
let bits = self.as_bits();
|
|
||||||
let mut naf = [0i8; 256];
|
|
||||||
for (b, bit) in bits.into_iter().enumerate() {
|
|
||||||
naf[b] = i8::try_from(bit).expect("bit didn't fit within an i8");
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0 .. 256 {
|
|
||||||
if naf[i] != 0 {
|
|
||||||
// if the bit is a one, work our way up through the window
|
|
||||||
// combining the bits with this bit.
|
|
||||||
for b in 1 .. 6 {
|
|
||||||
if (i + b) >= 256 {
|
|
||||||
// if we are at the length of the array then break out
|
|
||||||
// the loop.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// potential_carry - the value of the bit at i+b compared to the bit at i
|
|
||||||
let potential_carry = naf[i + b] << b;
|
|
||||||
|
|
||||||
if potential_carry != 0 {
|
|
||||||
if (naf[i] + potential_carry) <= 15 {
|
|
||||||
// if our current "bit" plus the potential carry is less than 16
|
|
||||||
// add it to our current "bit" and set the potential carry bit to 0.
|
|
||||||
naf[i] += potential_carry;
|
|
||||||
naf[i + b] = 0;
|
|
||||||
} else if (naf[i] - potential_carry) >= -15 {
|
|
||||||
// else if our current "bit" minus the potential carry is more than -16
|
|
||||||
// take it away from our current "bit".
|
|
||||||
// we then work our way up through the bits setting ones to zero, when
|
|
||||||
// we hit the first zero we change it to one then stop, this is to factor
|
|
||||||
// in the minus.
|
|
||||||
naf[i] -= potential_carry;
|
|
||||||
#[allow(clippy::needless_range_loop)]
|
|
||||||
for k in (i + b) .. 256 {
|
|
||||||
if naf[k] == 0 {
|
|
||||||
naf[k] = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
naf[k] = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
naf
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recover the scalar that an array of bytes was incorrectly interpreted as by ref10's `slide`
|
|
||||||
/// function (as used by the reference Monero implementation in C++).
|
|
||||||
///
|
|
||||||
/// For Borromean range proofs, Monero did not check the scalars used were reduced. This led to
|
|
||||||
/// some scalars serialized being interpreted as distinct scalars. This function recovers these
|
|
||||||
/// distinct scalars, as required to verify Borromean range proofs within the Monero protocol.
|
|
||||||
///
|
|
||||||
/// See <https://github.com/monero-project/monero/issues/8438> for more info.
|
|
||||||
//
|
|
||||||
/// This function does not execute in constant time and must only be used with public data.
|
|
||||||
pub fn ref10_slide_scalar_vartime(&self) -> Scalar {
|
|
||||||
if self.0[31] & 128 == 0 {
|
|
||||||
// Computing the w-NAF of a number can only give an output with 1 more bit than
|
|
||||||
// the number, so even if the number isn't reduced, the `slide` function will be
|
|
||||||
// correct when the last bit isn't set.
|
|
||||||
return Scalar::from_bytes_mod_order(self.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut recovered = Scalar::ZERO;
|
|
||||||
for &numb in self.non_adjacent_form().iter().rev() {
|
|
||||||
recovered += recovered;
|
|
||||||
match numb.cmp(&0) {
|
|
||||||
Ordering::Greater => {
|
|
||||||
recovered += PRECOMPUTED_SCALARS[usize::try_from(numb).expect("positive i8 -> usize") / 2]
|
|
||||||
}
|
|
||||||
Ordering::Less => {
|
|
||||||
recovered -=
|
|
||||||
PRECOMPUTED_SCALARS[usize::try_from(-numb).expect("negated negative i8 -> usize") / 2]
|
|
||||||
}
|
|
||||||
Ordering::Equal => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recovered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-borromean"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Borromean ring signatures arranged into a range proof, as done by the Monero protocol"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/borromean"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
# Cryptographic dependencies
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
# Other Monero dependencies
|
|
||||||
monero-io = { path = "../../io", version = "0.1", default-features = false }
|
|
||||||
monero-generators = { path = "../../generators", version = "0.4", default-features = false }
|
|
||||||
monero-primitives = { path = "../../primitives", version = "0.1", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
|
|
||||||
"monero-io/std",
|
|
||||||
"monero-generators/std",
|
|
||||||
"monero-primitives/std",
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Monero Borromean
|
|
||||||
|
|
||||||
Borromean ring signatures arranged into a range proof, as done by the Monero
|
|
||||||
protocol.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use core::fmt::Debug;
|
|
||||||
use std_shims::io::{self, Read, Write};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_io::*;
|
|
||||||
use monero_generators::H_pow_2;
|
|
||||||
use monero_primitives::{keccak256_to_scalar, UnreducedScalar};
|
|
||||||
|
|
||||||
// 64 Borromean ring signatures, as needed for a 64-bit range proof.
|
|
||||||
//
|
|
||||||
// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced.
|
|
||||||
// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction
|
|
||||||
// algorithm which was in use.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
struct BorromeanSignatures {
|
|
||||||
s0: [UnreducedScalar; 64],
|
|
||||||
s1: [UnreducedScalar; 64],
|
|
||||||
ee: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorromeanSignatures {
|
|
||||||
// Read a set of BorromeanSignatures.
|
|
||||||
fn read<R: Read>(r: &mut R) -> io::Result<BorromeanSignatures> {
|
|
||||||
Ok(BorromeanSignatures {
|
|
||||||
s0: read_array(UnreducedScalar::read, r)?,
|
|
||||||
s1: read_array(UnreducedScalar::read, r)?,
|
|
||||||
ee: read_scalar(r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the set of BorromeanSignatures.
|
|
||||||
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for s0 in &self.s0 {
|
|
||||||
s0.write(w)?;
|
|
||||||
}
|
|
||||||
for s1 in &self.s1 {
|
|
||||||
s1.write(w)?;
|
|
||||||
}
|
|
||||||
write_scalar(&self.ee, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify(&self, keys_a: &[EdwardsPoint], keys_b: &[EdwardsPoint]) -> bool {
|
|
||||||
let mut transcript = [0; 2048];
|
|
||||||
|
|
||||||
for i in 0 .. 64 {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let LL = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
|
||||||
&self.ee,
|
|
||||||
&keys_a[i],
|
|
||||||
&self.s0[i].ref10_slide_scalar_vartime(),
|
|
||||||
);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
|
||||||
&keccak256_to_scalar(LL.compress().as_bytes()),
|
|
||||||
&keys_b[i],
|
|
||||||
&self.s1[i].ref10_slide_scalar_vartime(),
|
|
||||||
);
|
|
||||||
transcript[(i * 32) .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
keccak256_to_scalar(transcript) == self.ee
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A range proof premised on Borromean ring signatures.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct BorromeanRange {
|
|
||||||
sigs: BorromeanSignatures,
|
|
||||||
bit_commitments: [EdwardsPoint; 64],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorromeanRange {
|
|
||||||
/// Read a BorromeanRange proof.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanRange> {
|
|
||||||
Ok(BorromeanRange {
|
|
||||||
sigs: BorromeanSignatures::read(r)?,
|
|
||||||
bit_commitments: read_array(read_point, r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the BorromeanRange proof.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.sigs.write(w)?;
|
|
||||||
write_raw_vec(write_point, &self.bit_commitments, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify the commitment contains a 64-bit value.
|
|
||||||
#[must_use]
|
|
||||||
pub fn verify(&self, commitment: &EdwardsPoint) -> bool {
|
|
||||||
if &self.bit_commitments.iter().sum::<EdwardsPoint>() != commitment {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let H_pow_2 = H_pow_2();
|
|
||||||
let mut commitments_sub_one = [EdwardsPoint::identity(); 64];
|
|
||||||
for i in 0 .. 64 {
|
|
||||||
commitments_sub_one[i] = self.bit_commitments[i] - H_pow_2[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sigs.verify(&self.bit_commitments, &commitments_sub_one)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-bulletproofs"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Bulletproofs(+) range proofs, as defined by the Monero protocol"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/bulletproofs"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
# Cryptographic dependencies
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
# Other Monero dependencies
|
|
||||||
monero-io = { path = "../../io", version = "0.1", default-features = false }
|
|
||||||
monero-generators = { path = "../../generators", version = "0.4", default-features = false }
|
|
||||||
monero-primitives = { path = "../../primitives", version = "0.1", default-features = false }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
monero-generators = { path = "../../generators", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex-literal = "0.4"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"rand_core/std",
|
|
||||||
"zeroize/std",
|
|
||||||
|
|
||||||
"monero-io/std",
|
|
||||||
"monero-generators/std",
|
|
||||||
"monero-primitives/std",
|
|
||||||
]
|
|
||||||
compile-time-generators = ["curve25519-dalek/precomputed-tables"]
|
|
||||||
default = ["std", "compile-time-generators"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Monero Bulletproofs(+)
|
|
||||||
|
|
||||||
Bulletproofs(+) range proofs, as defined by the Monero protocol.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
- `compile-time-generators` (on by default): Derives the generators at
|
|
||||||
compile-time so they don't need to be derived at runtime. This is recommended
|
|
||||||
if program size doesn't need to be kept minimal.
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
use std::{
|
|
||||||
io::Write,
|
|
||||||
env,
|
|
||||||
path::Path,
|
|
||||||
fs::{File, remove_file},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "compile-time-generators")]
|
|
||||||
fn generators(prefix: &'static str, path: &str) {
|
|
||||||
use curve25519_dalek::EdwardsPoint;
|
|
||||||
|
|
||||||
use monero_generators::bulletproofs_generators;
|
|
||||||
|
|
||||||
fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) {
|
|
||||||
for generator in points {
|
|
||||||
generators_string.extend(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
curve25519_dalek::edwards::CompressedEdwardsY({:?})
|
|
||||||
.decompress()
|
|
||||||
.expect(\"generator from build script wasn't on-curve\"),
|
|
||||||
",
|
|
||||||
generator.compress().to_bytes()
|
|
||||||
)
|
|
||||||
.chars(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let generators = bulletproofs_generators(prefix.as_bytes());
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut G_str = String::new();
|
|
||||||
serialize(&mut G_str, &generators.G);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut H_str = String::new();
|
|
||||||
serialize(&mut H_str, &generators.H);
|
|
||||||
|
|
||||||
let path = Path::new(&env::var("OUT_DIR").expect("cargo didn't set $OUT_DIR")).join(path);
|
|
||||||
let _ = remove_file(&path);
|
|
||||||
File::create(&path)
|
|
||||||
.expect("failed to create file in $OUT_DIR")
|
|
||||||
.write_all(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
pub(crate) static GENERATORS: LazyLock<Generators> = LazyLock::new(|| Generators {{
|
|
||||||
G: std_shims::vec![
|
|
||||||
{G_str}
|
|
||||||
],
|
|
||||||
H: std_shims::vec![
|
|
||||||
{H_str}
|
|
||||||
],
|
|
||||||
}});
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.expect("couldn't write generated source code to file on disk");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "compile-time-generators"))]
|
|
||||||
fn generators(prefix: &'static str, path: &str) {
|
|
||||||
let path = Path::new(&env::var("OUT_DIR").expect("cargo didn't set $OUT_DIR")).join(path);
|
|
||||||
let _ = remove_file(&path);
|
|
||||||
File::create(&path)
|
|
||||||
.expect("failed to create file in $OUT_DIR")
|
|
||||||
.write_all(
|
|
||||||
format!(
|
|
||||||
r#"
|
|
||||||
pub(crate) static GENERATORS: LazyLock<Generators> = LazyLock::new(|| {{
|
|
||||||
monero_generators::bulletproofs_generators(b"{prefix}")
|
|
||||||
}});
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.expect("couldn't write generated source code to file on disk");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
|
||||||
|
|
||||||
generators("bulletproof", "generators.rs");
|
|
||||||
generators("bulletproof_plus", "generators_plus.rs");
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use curve25519_dalek::{
|
|
||||||
constants::ED25519_BASEPOINT_POINT,
|
|
||||||
traits::{IsIdentity, VartimeMultiscalarMul},
|
|
||||||
scalar::Scalar,
|
|
||||||
edwards::EdwardsPoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_generators::{H as MONERO_H, Generators};
|
|
||||||
|
|
||||||
use crate::{original, plus};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct InternalBatchVerifier {
|
|
||||||
pub(crate) g: Scalar,
|
|
||||||
pub(crate) h: Scalar,
|
|
||||||
pub(crate) g_bold: Vec<Scalar>,
|
|
||||||
pub(crate) h_bold: Vec<Scalar>,
|
|
||||||
pub(crate) other: Vec<(Scalar, EdwardsPoint)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InternalBatchVerifier {
|
|
||||||
#[must_use]
|
|
||||||
fn verify(self, G: EdwardsPoint, H: EdwardsPoint, generators: &Generators) -> bool {
|
|
||||||
/*
|
|
||||||
Technically, this following line can overflow, and joining these `Vec`s _may_ panic if
|
|
||||||
they're individually acceptable lengths yet their sum isn't. This is so negligible, due to
|
|
||||||
the amount of memory required, it's dismissed.
|
|
||||||
*/
|
|
||||||
let capacity = 2 + self.g_bold.len() + self.h_bold.len() + self.other.len();
|
|
||||||
let mut scalars = Vec::with_capacity(capacity);
|
|
||||||
let mut points = Vec::with_capacity(capacity);
|
|
||||||
|
|
||||||
scalars.push(self.g);
|
|
||||||
points.push(G);
|
|
||||||
|
|
||||||
scalars.push(self.h);
|
|
||||||
points.push(H);
|
|
||||||
|
|
||||||
for (i, g_bold) in self.g_bold.into_iter().enumerate() {
|
|
||||||
scalars.push(g_bold);
|
|
||||||
points.push(generators.G[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, h_bold) in self.h_bold.into_iter().enumerate() {
|
|
||||||
scalars.push(h_bold);
|
|
||||||
points.push(generators.H[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (scalar, point) in self.other {
|
|
||||||
scalars.push(scalar);
|
|
||||||
points.push(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
EdwardsPoint::vartime_multiscalar_mul(scalars, points).is_identity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct BulletproofsBatchVerifier(pub(crate) InternalBatchVerifier);
|
|
||||||
impl BulletproofsBatchVerifier {
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn verify(self) -> bool {
|
|
||||||
self.0.verify(ED25519_BASEPOINT_POINT, *MONERO_H, &original::GENERATORS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct BulletproofsPlusBatchVerifier(pub(crate) InternalBatchVerifier);
|
|
||||||
impl BulletproofsPlusBatchVerifier {
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn verify(self) -> bool {
|
|
||||||
// Bulletproofs+ is written as per the paper, with G for the value and H for the mask
|
|
||||||
// Monero uses H for the value and G for the mask
|
|
||||||
self.0.verify(*MONERO_H, ED25519_BASEPOINT_POINT, &plus::GENERATORS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A batch verifier for Bulletproofs(+).
|
|
||||||
///
|
|
||||||
/// This uses a fixed layout such that all fixed points only incur a single point scaling,
|
|
||||||
/// regardless of the amounts of proofs verified. For all variable points (commitments), they're
|
|
||||||
/// accumulated with the fixed points into a single multiscalar multiplication.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct BatchVerifier {
|
|
||||||
pub(crate) original: BulletproofsBatchVerifier,
|
|
||||||
pub(crate) plus: BulletproofsPlusBatchVerifier,
|
|
||||||
}
|
|
||||||
impl BatchVerifier {
|
|
||||||
/// Create a new batch verifier.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
original: BulletproofsBatchVerifier(InternalBatchVerifier::default()),
|
|
||||||
plus: BulletproofsPlusBatchVerifier(InternalBatchVerifier::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify all of the proofs queued within this batch verifier.
|
|
||||||
///
|
|
||||||
/// This uses a variable-time multiscalar multiplication internally.
|
|
||||||
#[must_use]
|
|
||||||
pub fn verify(self) -> bool {
|
|
||||||
self.original.verify() && self.plus.verify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
use std_shims::{vec, vec::Vec};
|
|
||||||
|
|
||||||
use curve25519_dalek::{
|
|
||||||
traits::{MultiscalarMul, VartimeMultiscalarMul},
|
|
||||||
scalar::Scalar,
|
|
||||||
edwards::EdwardsPoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) use monero_generators::{MAX_COMMITMENTS, COMMITMENT_BITS};
|
|
||||||
|
|
||||||
pub(crate) fn multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
|
|
||||||
let mut buf_scalars = Vec::with_capacity(pairs.len());
|
|
||||||
let mut buf_points = Vec::with_capacity(pairs.len());
|
|
||||||
for (scalar, point) in pairs {
|
|
||||||
buf_scalars.push(scalar);
|
|
||||||
buf_points.push(point);
|
|
||||||
}
|
|
||||||
EdwardsPoint::multiscalar_mul(buf_scalars, buf_points)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn multiexp_vartime(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
|
|
||||||
let mut buf_scalars = Vec::with_capacity(pairs.len());
|
|
||||||
let mut buf_points = Vec::with_capacity(pairs.len());
|
|
||||||
for (scalar, point) in pairs {
|
|
||||||
buf_scalars.push(scalar);
|
|
||||||
buf_points.push(point);
|
|
||||||
}
|
|
||||||
EdwardsPoint::vartime_multiscalar_mul(buf_scalars, buf_points)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This has room for optimization worth investigating further. It currently takes
|
|
||||||
an iterative approach. It can be optimized further via divide and conquer.
|
|
||||||
|
|
||||||
Assume there are 4 challenges.
|
|
||||||
|
|
||||||
Iterative approach (current):
|
|
||||||
1. Do the optimal multiplications across challenge column 0 and 1.
|
|
||||||
2. Do the optimal multiplications across that result and column 2.
|
|
||||||
3. Do the optimal multiplications across that result and column 3.
|
|
||||||
|
|
||||||
Divide and conquer (worth investigating further):
|
|
||||||
1. Do the optimal multiplications across challenge column 0 and 1.
|
|
||||||
2. Do the optimal multiplications across challenge column 2 and 3.
|
|
||||||
3. Multiply both results together.
|
|
||||||
|
|
||||||
When there are 4 challenges (n=16), the iterative approach does 28 multiplications
|
|
||||||
versus divide and conquer's 24.
|
|
||||||
*/
|
|
||||||
pub(crate) fn challenge_products(challenges: &[(Scalar, Scalar)]) -> Vec<Scalar> {
|
|
||||||
let mut products = vec![Scalar::ONE; 1 << challenges.len()];
|
|
||||||
|
|
||||||
if !challenges.is_empty() {
|
|
||||||
products[0] = challenges[0].1;
|
|
||||||
products[1] = challenges[0].0;
|
|
||||||
|
|
||||||
for (j, challenge) in challenges.iter().enumerate().skip(1) {
|
|
||||||
let mut slots = (1 << (j + 1)) - 1;
|
|
||||||
while slots > 0 {
|
|
||||||
products[slots] = products[slots / 2] * challenge.0;
|
|
||||||
products[slots - 1] = products[slots / 2] * challenge.1;
|
|
||||||
|
|
||||||
slots = slots.saturating_sub(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity check since if the above failed to populate, it'd be critical
|
|
||||||
for product in &products {
|
|
||||||
debug_assert!(*product != Scalar::ZERO);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
products
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use std_shims::{
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
|
||||||
|
|
||||||
use monero_io::*;
|
|
||||||
pub use monero_generators::MAX_COMMITMENTS;
|
|
||||||
use monero_generators::COMMITMENT_BITS;
|
|
||||||
use monero_primitives::Commitment;
|
|
||||||
|
|
||||||
pub(crate) mod scalar_vector;
|
|
||||||
pub(crate) mod point_vector;
|
|
||||||
|
|
||||||
pub(crate) mod core;
|
|
||||||
|
|
||||||
pub(crate) mod batch_verifier;
|
|
||||||
use batch_verifier::{BulletproofsBatchVerifier, BulletproofsPlusBatchVerifier};
|
|
||||||
pub use batch_verifier::BatchVerifier;
|
|
||||||
|
|
||||||
pub(crate) mod original;
|
|
||||||
use crate::original::{
|
|
||||||
IpProof, AggregateRangeStatement as OriginalStatement, AggregateRangeWitness as OriginalWitness,
|
|
||||||
AggregateRangeProof as OriginalProof,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) mod plus;
|
|
||||||
use crate::plus::{
|
|
||||||
WipProof, AggregateRangeStatement as PlusStatement, AggregateRangeWitness as PlusWitness,
|
|
||||||
AggregateRangeProof as PlusProof,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
// The logarithm (over 2) of the amount of bits a value within a commitment may use.
|
|
||||||
const LOG_COMMITMENT_BITS: usize = COMMITMENT_BITS.ilog2() as usize;
|
|
||||||
// The maximum length of L/R `Vec`s.
|
|
||||||
const MAX_LR: usize = (MAX_COMMITMENTS.ilog2() as usize) + LOG_COMMITMENT_BITS;
|
|
||||||
|
|
||||||
/// An error from proving/verifying Bulletproofs(+).
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum BulletproofError {
|
|
||||||
/// Proving/verifying a Bulletproof(+) range proof with no commitments.
|
|
||||||
#[cfg_attr(feature = "std", error("no commitments to prove the range for"))]
|
|
||||||
NoCommitments,
|
|
||||||
/// Proving/verifying a Bulletproof(+) range proof with more commitments than supported.
|
|
||||||
#[cfg_attr(feature = "std", error("too many commitments to prove the range for"))]
|
|
||||||
TooManyCommitments,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Bulletproof(+).
|
|
||||||
///
|
|
||||||
/// This encapsulates either a Bulletproof or a Bulletproof+.
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum Bulletproof {
|
|
||||||
/// A Bulletproof.
|
|
||||||
Original(OriginalProof),
|
|
||||||
/// A Bulletproof+.
|
|
||||||
Plus(PlusProof),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bulletproof {
|
|
||||||
fn bp_fields(plus: bool) -> usize {
|
|
||||||
if plus {
|
|
||||||
6
|
|
||||||
} else {
|
|
||||||
9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the weight penalty for the Bulletproof(+).
|
|
||||||
///
|
|
||||||
/// Bulletproofs(+) are logarithmically sized yet linearly timed. Evaluating by their size alone
|
|
||||||
/// accordingly doesn't properly represent the burden of the proof. Monero 'claws back' some of
|
|
||||||
/// the weight lost by using a proof smaller than it is fast to compensate for this.
|
|
||||||
///
|
|
||||||
/// If the amount of outputs specified exceeds the maximum amount of outputs, the result for the
|
|
||||||
/// maximum amount of outputs will be returned.
|
|
||||||
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
|
||||||
// src/cryptonote_basic/cryptonote_format_utils.cpp#L106-L124
|
|
||||||
pub fn calculate_clawback(plus: bool, n_outputs: usize) -> (usize, usize) {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut LR_len = 0;
|
|
||||||
let mut n_padded_outputs = 1;
|
|
||||||
while n_padded_outputs < n_outputs.min(MAX_COMMITMENTS) {
|
|
||||||
LR_len += 1;
|
|
||||||
n_padded_outputs = 1 << LR_len;
|
|
||||||
}
|
|
||||||
LR_len += LOG_COMMITMENT_BITS;
|
|
||||||
|
|
||||||
let mut clawback = 0;
|
|
||||||
if n_padded_outputs > 2 {
|
|
||||||
let fields = Bulletproof::bp_fields(plus);
|
|
||||||
let base = ((fields + (2 * (LOG_COMMITMENT_BITS + 1))) * 32) / 2;
|
|
||||||
let size = (fields + (2 * LR_len)) * 32;
|
|
||||||
clawback = ((base * n_padded_outputs) - size) * 4 / 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
(clawback, LR_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof.
|
|
||||||
pub fn prove<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
outputs: Vec<Commitment>,
|
|
||||||
) -> Result<Bulletproof, BulletproofError> {
|
|
||||||
if outputs.is_empty() {
|
|
||||||
Err(BulletproofError::NoCommitments)?;
|
|
||||||
}
|
|
||||||
if outputs.len() > MAX_COMMITMENTS {
|
|
||||||
Err(BulletproofError::TooManyCommitments)?;
|
|
||||||
}
|
|
||||||
let commitments = outputs.iter().map(Commitment::calculate).collect::<Vec<_>>();
|
|
||||||
Ok(Bulletproof::Original(
|
|
||||||
OriginalStatement::new(&commitments)
|
|
||||||
.expect("failed to create statement despite checking amount of commitments")
|
|
||||||
.prove(
|
|
||||||
rng,
|
|
||||||
OriginalWitness::new(outputs)
|
|
||||||
.expect("failed to create witness despite checking amount of commitments"),
|
|
||||||
)
|
|
||||||
.expect(
|
|
||||||
"failed to prove Bulletproof::Original despite ensuring statement/witness consistency",
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof+.
|
|
||||||
pub fn prove_plus<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
outputs: Vec<Commitment>,
|
|
||||||
) -> Result<Bulletproof, BulletproofError> {
|
|
||||||
if outputs.is_empty() {
|
|
||||||
Err(BulletproofError::NoCommitments)?;
|
|
||||||
}
|
|
||||||
if outputs.len() > MAX_COMMITMENTS {
|
|
||||||
Err(BulletproofError::TooManyCommitments)?;
|
|
||||||
}
|
|
||||||
let commitments = outputs.iter().map(Commitment::calculate).collect::<Vec<_>>();
|
|
||||||
Ok(Bulletproof::Plus(
|
|
||||||
PlusStatement::new(&commitments)
|
|
||||||
.expect("failed to create statement despite checking amount of commitments")
|
|
||||||
.prove(
|
|
||||||
rng,
|
|
||||||
&Zeroizing::new(
|
|
||||||
PlusWitness::new(outputs)
|
|
||||||
.expect("failed to create witness despite checking amount of commitments"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.expect("failed to prove Bulletproof::Plus despite ensuring statement/witness consistency"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify the given Bulletproof(+).
|
|
||||||
#[must_use]
|
|
||||||
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
|
|
||||||
match self {
|
|
||||||
Bulletproof::Original(bp) => {
|
|
||||||
let mut verifier = BulletproofsBatchVerifier::default();
|
|
||||||
let Some(statement) = OriginalStatement::new(commitments) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if !statement.verify(rng, &mut verifier, bp.clone()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
verifier.verify()
|
|
||||||
}
|
|
||||||
Bulletproof::Plus(bp) => {
|
|
||||||
let mut verifier = BulletproofsPlusBatchVerifier::default();
|
|
||||||
let Some(statement) = PlusStatement::new(commitments) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if !statement.verify(rng, &mut verifier, bp.clone()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
verifier.verify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accumulate the verification for the given Bulletproof(+) into the specified BatchVerifier.
|
|
||||||
///
|
|
||||||
/// Returns false if the Bulletproof(+) isn't sane, leaving the BatchVerifier in an undefined
|
|
||||||
/// state.
|
|
||||||
///
|
|
||||||
/// Returns true if the Bulletproof(+) is sane, regardless of its validity.
|
|
||||||
///
|
|
||||||
/// The BatchVerifier must have its verification function executed to actually verify this proof.
|
|
||||||
#[must_use]
|
|
||||||
pub fn batch_verify<R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier,
|
|
||||||
commitments: &[EdwardsPoint],
|
|
||||||
) -> bool {
|
|
||||||
match self {
|
|
||||||
Bulletproof::Original(bp) => {
|
|
||||||
let Some(statement) = OriginalStatement::new(commitments) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
statement.verify(rng, &mut verifier.original, bp.clone())
|
|
||||||
}
|
|
||||||
Bulletproof::Plus(bp) => {
|
|
||||||
let Some(statement) = PlusStatement::new(commitments) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
statement.verify(rng, &mut verifier.plus, bp.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_core<W: Write, F: Fn(&[EdwardsPoint], &mut W) -> io::Result<()>>(
|
|
||||||
&self,
|
|
||||||
w: &mut W,
|
|
||||||
specific_write_vec: F,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Bulletproof::Original(bp) => {
|
|
||||||
write_point(&bp.A, w)?;
|
|
||||||
write_point(&bp.S, w)?;
|
|
||||||
write_point(&bp.T1, w)?;
|
|
||||||
write_point(&bp.T2, w)?;
|
|
||||||
write_scalar(&bp.tau_x, w)?;
|
|
||||||
write_scalar(&bp.mu, w)?;
|
|
||||||
specific_write_vec(&bp.ip.L, w)?;
|
|
||||||
specific_write_vec(&bp.ip.R, w)?;
|
|
||||||
write_scalar(&bp.ip.a, w)?;
|
|
||||||
write_scalar(&bp.ip.b, w)?;
|
|
||||||
write_scalar(&bp.t_hat, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
Bulletproof::Plus(bp) => {
|
|
||||||
write_point(&bp.A, w)?;
|
|
||||||
write_point(&bp.wip.A, w)?;
|
|
||||||
write_point(&bp.wip.B, w)?;
|
|
||||||
write_scalar(&bp.wip.r_answer, w)?;
|
|
||||||
write_scalar(&bp.wip.s_answer, w)?;
|
|
||||||
write_scalar(&bp.wip.delta_answer, w)?;
|
|
||||||
specific_write_vec(&bp.wip.L, w)?;
|
|
||||||
specific_write_vec(&bp.wip.R, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a Bulletproof(+) for the message signed by a transaction's signature.
|
|
||||||
///
|
|
||||||
/// This has a distinct encoding from the standard encoding.
|
|
||||||
pub fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.write_core(w, |points, w| write_raw_vec(write_point, points, w))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a Bulletproof(+).
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.write_core(w, |points, w| write_vec(write_point, points, w))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize a Bulletproof(+) to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = Vec::with_capacity(512);
|
|
||||||
self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Bulletproof.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Bulletproof> {
|
|
||||||
Ok(Bulletproof::Original(OriginalProof {
|
|
||||||
A: read_point(r)?,
|
|
||||||
S: read_point(r)?,
|
|
||||||
T1: read_point(r)?,
|
|
||||||
T2: read_point(r)?,
|
|
||||||
tau_x: read_scalar(r)?,
|
|
||||||
mu: read_scalar(r)?,
|
|
||||||
ip: IpProof {
|
|
||||||
L: read_vec(read_point, Some(MAX_LR), r)?,
|
|
||||||
R: read_vec(read_point, Some(MAX_LR), r)?,
|
|
||||||
a: read_scalar(r)?,
|
|
||||||
b: read_scalar(r)?,
|
|
||||||
},
|
|
||||||
t_hat: read_scalar(r)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Bulletproof+.
|
|
||||||
pub fn read_plus<R: Read>(r: &mut R) -> io::Result<Bulletproof> {
|
|
||||||
Ok(Bulletproof::Plus(PlusProof {
|
|
||||||
A: read_point(r)?,
|
|
||||||
wip: WipProof {
|
|
||||||
A: read_point(r)?,
|
|
||||||
B: read_point(r)?,
|
|
||||||
r_answer: read_scalar(r)?,
|
|
||||||
s_answer: read_scalar(r)?,
|
|
||||||
delta_answer: read_scalar(r)?,
|
|
||||||
L: read_vec(read_point, Some(MAX_LR), r)?.into_iter().collect(),
|
|
||||||
R: read_vec(read_point, Some(MAX_LR), r)?.into_iter().collect(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
use std_shims::{vec, vec::Vec};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_generators::H;
|
|
||||||
use monero_primitives::{INV_EIGHT, keccak256_to_scalar};
|
|
||||||
use crate::{
|
|
||||||
core::{multiexp_vartime, challenge_products},
|
|
||||||
scalar_vector::ScalarVector,
|
|
||||||
point_vector::PointVector,
|
|
||||||
BulletproofsBatchVerifier,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An error from proving/verifying Inner-Product statements.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
pub(crate) enum IpError {
|
|
||||||
IncorrectAmountOfGenerators,
|
|
||||||
DifferingLrLengths,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Bulletproofs Inner-Product statement.
|
|
||||||
///
|
|
||||||
/// This is for usage with Protocol 2 from the Bulletproofs paper.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct IpStatement {
|
|
||||||
// Weights for h_bold
|
|
||||||
h_bold_weights: ScalarVector,
|
|
||||||
// u as the discrete logarithm of G
|
|
||||||
u: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The witness for the Bulletproofs Inner-Product statement.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct IpWitness {
|
|
||||||
// a
|
|
||||||
a: ScalarVector,
|
|
||||||
// b
|
|
||||||
b: ScalarVector,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpWitness {
|
|
||||||
/// Construct a new witness for an Inner-Product statement.
|
|
||||||
///
|
|
||||||
/// This functions return None if the lengths of a, b are mismatched, not a power of two, or are
|
|
||||||
/// empty.
|
|
||||||
pub(crate) fn new(a: ScalarVector, b: ScalarVector) -> Option<Self> {
|
|
||||||
if a.0.is_empty() || (a.len() != b.len()) {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut power_of_2 = 1;
|
|
||||||
while power_of_2 < a.len() {
|
|
||||||
power_of_2 <<= 1;
|
|
||||||
}
|
|
||||||
if power_of_2 != a.len() {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Self { a, b })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A proof for the Bulletproofs Inner-Product statement.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub(crate) struct IpProof {
|
|
||||||
pub(crate) L: Vec<EdwardsPoint>,
|
|
||||||
pub(crate) R: Vec<EdwardsPoint>,
|
|
||||||
pub(crate) a: Scalar,
|
|
||||||
pub(crate) b: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpStatement {
|
|
||||||
/// Create a new Inner-Product statement which won't transcript P.
|
|
||||||
///
|
|
||||||
/// This MUST only be called when P is deterministic to already transcripted elements.
|
|
||||||
pub(crate) fn new_without_P_transcript(h_bold_weights: ScalarVector, u: Scalar) -> Self {
|
|
||||||
Self { h_bold_weights, u }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transcript a round of the protocol
|
|
||||||
fn transcript_L_R(transcript: Scalar, L: EdwardsPoint, R: EdwardsPoint) -> Scalar {
|
|
||||||
let mut transcript = transcript.to_bytes().to_vec();
|
|
||||||
transcript.extend(L.compress().to_bytes());
|
|
||||||
transcript.extend(R.compress().to_bytes());
|
|
||||||
keccak256_to_scalar(transcript)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove for this Inner-Product statement.
|
|
||||||
///
|
|
||||||
/// Returns an error if this statement couldn't be proven for (such as if the witness isn't
|
|
||||||
/// consistent).
|
|
||||||
pub(crate) fn prove(
|
|
||||||
self,
|
|
||||||
mut transcript: Scalar,
|
|
||||||
witness: IpWitness,
|
|
||||||
) -> Result<IpProof, IpError> {
|
|
||||||
let generators = &crate::original::GENERATORS;
|
|
||||||
let g_bold_slice = &generators.G[.. witness.a.len()];
|
|
||||||
let h_bold_slice = &generators.H[.. witness.a.len()];
|
|
||||||
|
|
||||||
let (mut g_bold, mut h_bold, u, mut a, mut b) = {
|
|
||||||
let IpStatement { h_bold_weights, u } = self;
|
|
||||||
let u = *H * u;
|
|
||||||
|
|
||||||
// Ensure we have the exact amount of weights
|
|
||||||
if h_bold_weights.len() != g_bold_slice.len() {
|
|
||||||
Err(IpError::IncorrectAmountOfGenerators)?;
|
|
||||||
}
|
|
||||||
// Acquire a local copy of the generators
|
|
||||||
let g_bold = PointVector(g_bold_slice.to_vec());
|
|
||||||
let h_bold = PointVector(h_bold_slice.to_vec()).mul_vec(&h_bold_weights);
|
|
||||||
|
|
||||||
let IpWitness { a, b } = witness;
|
|
||||||
|
|
||||||
(g_bold, h_bold, u, a, b)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut L_vec = vec![];
|
|
||||||
let mut R_vec = vec![];
|
|
||||||
|
|
||||||
// `else: (n > 1)` case, lines 18-35 of the Bulletproofs paper
|
|
||||||
// This interprets `g_bold.len()` as `n`
|
|
||||||
while g_bold.len() > 1 {
|
|
||||||
// Split a, b, g_bold, h_bold as needed for lines 20-24
|
|
||||||
let (a1, a2) = a.clone().split();
|
|
||||||
let (b1, b2) = b.clone().split();
|
|
||||||
|
|
||||||
let (g_bold1, g_bold2) = g_bold.split();
|
|
||||||
let (h_bold1, h_bold2) = h_bold.split();
|
|
||||||
|
|
||||||
let n_hat = g_bold1.len();
|
|
||||||
|
|
||||||
// Sanity
|
|
||||||
debug_assert_eq!(a1.len(), n_hat);
|
|
||||||
debug_assert_eq!(a2.len(), n_hat);
|
|
||||||
debug_assert_eq!(b1.len(), n_hat);
|
|
||||||
debug_assert_eq!(b2.len(), n_hat);
|
|
||||||
debug_assert_eq!(g_bold1.len(), n_hat);
|
|
||||||
debug_assert_eq!(g_bold2.len(), n_hat);
|
|
||||||
debug_assert_eq!(h_bold1.len(), n_hat);
|
|
||||||
debug_assert_eq!(h_bold2.len(), n_hat);
|
|
||||||
|
|
||||||
// cl, cr, lines 21-22
|
|
||||||
let cl = a1.clone().inner_product(&b2);
|
|
||||||
let cr = a2.clone().inner_product(&b1);
|
|
||||||
|
|
||||||
let L = {
|
|
||||||
let mut L_terms = Vec::with_capacity(1 + (2 * g_bold1.len()));
|
|
||||||
for (a, g) in a1.0.iter().zip(g_bold2.0.iter()) {
|
|
||||||
L_terms.push((*a, *g));
|
|
||||||
}
|
|
||||||
for (b, h) in b2.0.iter().zip(h_bold1.0.iter()) {
|
|
||||||
L_terms.push((*b, *h));
|
|
||||||
}
|
|
||||||
L_terms.push((cl, u));
|
|
||||||
// Uses vartime since this isn't a ZK proof
|
|
||||||
multiexp_vartime(&L_terms)
|
|
||||||
};
|
|
||||||
L_vec.push(L * INV_EIGHT());
|
|
||||||
|
|
||||||
let R = {
|
|
||||||
let mut R_terms = Vec::with_capacity(1 + (2 * g_bold1.len()));
|
|
||||||
for (a, g) in a2.0.iter().zip(g_bold1.0.iter()) {
|
|
||||||
R_terms.push((*a, *g));
|
|
||||||
}
|
|
||||||
for (b, h) in b1.0.iter().zip(h_bold2.0.iter()) {
|
|
||||||
R_terms.push((*b, *h));
|
|
||||||
}
|
|
||||||
R_terms.push((cr, u));
|
|
||||||
multiexp_vartime(&R_terms)
|
|
||||||
};
|
|
||||||
R_vec.push(R * INV_EIGHT());
|
|
||||||
|
|
||||||
// Now that we've calculate L, R, transcript them to receive x (26-27)
|
|
||||||
transcript = Self::transcript_L_R(
|
|
||||||
transcript,
|
|
||||||
*L_vec.last().expect("couldn't get last L_vec despite always being non-empty"),
|
|
||||||
*R_vec.last().expect("couldn't get last R_vec despite always being non-empty"),
|
|
||||||
);
|
|
||||||
let x = transcript;
|
|
||||||
let x_inv = x.invert();
|
|
||||||
|
|
||||||
// The prover and verifier now calculate the following (28-31)
|
|
||||||
g_bold = PointVector(Vec::with_capacity(g_bold1.len()));
|
|
||||||
for (a, b) in g_bold1.0.into_iter().zip(g_bold2.0.into_iter()) {
|
|
||||||
g_bold.0.push(multiexp_vartime(&[(x_inv, a), (x, b)]));
|
|
||||||
}
|
|
||||||
h_bold = PointVector(Vec::with_capacity(h_bold1.len()));
|
|
||||||
for (a, b) in h_bold1.0.into_iter().zip(h_bold2.0.into_iter()) {
|
|
||||||
h_bold.0.push(multiexp_vartime(&[(x, a), (x_inv, b)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 32-34
|
|
||||||
a = (a1 * x) + &(a2 * x_inv);
|
|
||||||
b = (b1 * x_inv) + &(b2 * x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `if n = 1` case from line 14-17
|
|
||||||
|
|
||||||
// Sanity
|
|
||||||
debug_assert_eq!(g_bold.len(), 1);
|
|
||||||
debug_assert_eq!(h_bold.len(), 1);
|
|
||||||
debug_assert_eq!(a.len(), 1);
|
|
||||||
debug_assert_eq!(b.len(), 1);
|
|
||||||
|
|
||||||
// We simply send a/b
|
|
||||||
Ok(IpProof { L: L_vec, R: R_vec, a: a[0], b: b[0] })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Queue an Inner-Product proof for batch verification.
|
|
||||||
///
|
|
||||||
/// This will return Err if there is an error. This will return Ok if the proof was successfully
|
|
||||||
/// queued for batch verification. The caller is required to verify the batch in order to ensure
|
|
||||||
/// the proof is actually correct.
|
|
||||||
pub(crate) fn verify(
|
|
||||||
self,
|
|
||||||
verifier: &mut BulletproofsBatchVerifier,
|
|
||||||
ip_rows: usize,
|
|
||||||
mut transcript: Scalar,
|
|
||||||
verifier_weight: Scalar,
|
|
||||||
proof: IpProof,
|
|
||||||
) -> Result<(), IpError> {
|
|
||||||
let generators = &crate::original::GENERATORS;
|
|
||||||
let g_bold_slice = &generators.G[.. ip_rows];
|
|
||||||
let h_bold_slice = &generators.H[.. ip_rows];
|
|
||||||
|
|
||||||
let IpStatement { h_bold_weights, u } = self;
|
|
||||||
|
|
||||||
// Verify the L/R lengths
|
|
||||||
{
|
|
||||||
// Calculate the discrete log w.r.t. 2 for the amount of generators present
|
|
||||||
let mut lr_len = 0;
|
|
||||||
while (1 << lr_len) < g_bold_slice.len() {
|
|
||||||
lr_len += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This proof has less/more terms than the passed in generators are for
|
|
||||||
if proof.L.len() != lr_len {
|
|
||||||
Err(IpError::IncorrectAmountOfGenerators)?;
|
|
||||||
}
|
|
||||||
if proof.L.len() != proof.R.len() {
|
|
||||||
Err(IpError::DifferingLrLengths)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Again, we start with the `else: (n > 1)` case
|
|
||||||
|
|
||||||
// We need x, x_inv per lines 25-27 for lines 28-31
|
|
||||||
let mut xs = Vec::with_capacity(proof.L.len());
|
|
||||||
for (L, R) in proof.L.iter().zip(proof.R.iter()) {
|
|
||||||
transcript = Self::transcript_L_R(transcript, *L, *R);
|
|
||||||
xs.push(transcript);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We calculate their inverse in batch
|
|
||||||
let mut x_invs = xs.clone();
|
|
||||||
Scalar::batch_invert(&mut x_invs);
|
|
||||||
|
|
||||||
// Now, with x and x_inv, we need to calculate g_bold', h_bold', P'
|
|
||||||
//
|
|
||||||
// For the sake of performance, we solely want to calculate all of these in terms of scalings
|
|
||||||
// for g_bold, h_bold, P, and don't want to actually perform intermediary scalings of the
|
|
||||||
// points
|
|
||||||
//
|
|
||||||
// L and R are easy, as it's simply x**2, x**-2
|
|
||||||
//
|
|
||||||
// For the series of g_bold, h_bold, we use the `challenge_products` function
|
|
||||||
// For how that works, please see its own documentation
|
|
||||||
let product_cache = {
|
|
||||||
let mut challenges = Vec::with_capacity(proof.L.len());
|
|
||||||
|
|
||||||
let x_iter = xs.into_iter().zip(x_invs);
|
|
||||||
let lr_iter = proof.L.into_iter().zip(proof.R);
|
|
||||||
for ((x, x_inv), (L, R)) in x_iter.zip(lr_iter) {
|
|
||||||
challenges.push((x, x_inv));
|
|
||||||
verifier.0.other.push((verifier_weight * (x * x), L.mul_by_cofactor()));
|
|
||||||
verifier.0.other.push((verifier_weight * (x_inv * x_inv), R.mul_by_cofactor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge_products(&challenges)
|
|
||||||
};
|
|
||||||
|
|
||||||
// And now for the `if n = 1` case
|
|
||||||
let c = proof.a * proof.b;
|
|
||||||
|
|
||||||
// The multiexp of these terms equate to the final permutation of P
|
|
||||||
// We now add terms for a * g_bold' + b * h_bold' b + c * u, with the scalars negative such
|
|
||||||
// that the terms sum to 0 for an honest prover
|
|
||||||
|
|
||||||
// The g_bold * a term case from line 16
|
|
||||||
#[allow(clippy::needless_range_loop)]
|
|
||||||
for i in 0 .. g_bold_slice.len() {
|
|
||||||
verifier.0.g_bold[i] -= verifier_weight * product_cache[i] * proof.a;
|
|
||||||
}
|
|
||||||
// The h_bold * b term case from line 16
|
|
||||||
for i in 0 .. h_bold_slice.len() {
|
|
||||||
verifier.0.h_bold[i] -=
|
|
||||||
verifier_weight * product_cache[product_cache.len() - 1 - i] * proof.b * h_bold_weights[i];
|
|
||||||
}
|
|
||||||
// The c * u term case from line 16
|
|
||||||
verifier.0.h -= verifier_weight * c * u;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
use std_shims::{sync::LazyLock, vec::Vec};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_generators::{H as MONERO_H, Generators, MAX_COMMITMENTS, COMMITMENT_BITS};
|
|
||||||
use monero_primitives::{Commitment, INV_EIGHT, keccak256_to_scalar};
|
|
||||||
use crate::{core::multiexp, scalar_vector::ScalarVector, BulletproofsBatchVerifier};
|
|
||||||
|
|
||||||
pub(crate) mod inner_product;
|
|
||||||
use inner_product::*;
|
|
||||||
pub(crate) use inner_product::IpProof;
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/generators.rs"));
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct AggregateRangeStatement<'a> {
|
|
||||||
commitments: &'a [EdwardsPoint],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct AggregateRangeWitness {
|
|
||||||
commitments: Vec<Commitment>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct AggregateRangeProof {
|
|
||||||
pub(crate) A: EdwardsPoint,
|
|
||||||
pub(crate) S: EdwardsPoint,
|
|
||||||
pub(crate) T1: EdwardsPoint,
|
|
||||||
pub(crate) T2: EdwardsPoint,
|
|
||||||
pub(crate) tau_x: Scalar,
|
|
||||||
pub(crate) mu: Scalar,
|
|
||||||
pub(crate) t_hat: Scalar,
|
|
||||||
pub(crate) ip: IpProof,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> AggregateRangeStatement<'a> {
|
|
||||||
pub(crate) fn new(commitments: &'a [EdwardsPoint]) -> Option<Self> {
|
|
||||||
if commitments.is_empty() || (commitments.len() > MAX_COMMITMENTS) {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
Some(Self { commitments })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AggregateRangeWitness {
|
|
||||||
pub(crate) fn new(commitments: Vec<Commitment>) -> Option<Self> {
|
|
||||||
if commitments.is_empty() || (commitments.len() > MAX_COMMITMENTS) {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
Some(Self { commitments })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> AggregateRangeStatement<'a> {
|
|
||||||
fn initial_transcript(&self) -> (Scalar, Vec<EdwardsPoint>) {
|
|
||||||
let V = self.commitments.iter().map(|c| c * INV_EIGHT()).collect::<Vec<_>>();
|
|
||||||
(keccak256_to_scalar(V.iter().flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>()), V)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_A_S(transcript: Scalar, A: EdwardsPoint, S: EdwardsPoint) -> (Scalar, Scalar) {
|
|
||||||
let mut buf = Vec::with_capacity(96);
|
|
||||||
buf.extend(transcript.to_bytes());
|
|
||||||
buf.extend(A.compress().to_bytes());
|
|
||||||
buf.extend(S.compress().to_bytes());
|
|
||||||
let y = keccak256_to_scalar(buf);
|
|
||||||
let z = keccak256_to_scalar(y.to_bytes());
|
|
||||||
(y, z)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_T12(transcript: Scalar, T1: EdwardsPoint, T2: EdwardsPoint) -> Scalar {
|
|
||||||
let mut buf = Vec::with_capacity(128);
|
|
||||||
buf.extend(transcript.to_bytes());
|
|
||||||
buf.extend(transcript.to_bytes());
|
|
||||||
buf.extend(T1.compress().to_bytes());
|
|
||||||
buf.extend(T2.compress().to_bytes());
|
|
||||||
keccak256_to_scalar(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_tau_x_mu_t_hat(
|
|
||||||
transcript: Scalar,
|
|
||||||
tau_x: Scalar,
|
|
||||||
mu: Scalar,
|
|
||||||
t_hat: Scalar,
|
|
||||||
) -> Scalar {
|
|
||||||
let mut buf = Vec::with_capacity(128);
|
|
||||||
buf.extend(transcript.to_bytes());
|
|
||||||
buf.extend(transcript.to_bytes());
|
|
||||||
buf.extend(tau_x.to_bytes());
|
|
||||||
buf.extend(mu.to_bytes());
|
|
||||||
buf.extend(t_hat.to_bytes());
|
|
||||||
keccak256_to_scalar(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
pub(crate) fn prove(
|
|
||||||
self,
|
|
||||||
rng: &mut (impl RngCore + CryptoRng),
|
|
||||||
witness: AggregateRangeWitness,
|
|
||||||
) -> Option<AggregateRangeProof> {
|
|
||||||
if self.commitments != witness.commitments.iter().map(Commitment::calculate).collect::<Vec<_>>()
|
|
||||||
{
|
|
||||||
None?
|
|
||||||
};
|
|
||||||
|
|
||||||
let generators = &GENERATORS;
|
|
||||||
|
|
||||||
let (mut transcript, _) = self.initial_transcript();
|
|
||||||
|
|
||||||
// Find out the padded amount of commitments
|
|
||||||
let mut padded_pow_of_2 = 1;
|
|
||||||
while padded_pow_of_2 < witness.commitments.len() {
|
|
||||||
padded_pow_of_2 <<= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut aL = ScalarVector::new(padded_pow_of_2 * COMMITMENT_BITS);
|
|
||||||
for (i, commitment) in witness.commitments.iter().enumerate() {
|
|
||||||
let mut amount = commitment.amount;
|
|
||||||
for j in 0 .. COMMITMENT_BITS {
|
|
||||||
aL[(i * COMMITMENT_BITS) + j] = Scalar::from(amount & 1);
|
|
||||||
amount >>= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let aR = aL.clone() - Scalar::ONE;
|
|
||||||
|
|
||||||
let alpha = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let A = {
|
|
||||||
let mut terms = Vec::with_capacity(1 + (2 * aL.len()));
|
|
||||||
terms.push((alpha, ED25519_BASEPOINT_POINT));
|
|
||||||
for (aL, G) in aL.0.iter().zip(&generators.G) {
|
|
||||||
terms.push((*aL, *G));
|
|
||||||
}
|
|
||||||
for (aR, H) in aR.0.iter().zip(&generators.H) {
|
|
||||||
terms.push((*aR, *H));
|
|
||||||
}
|
|
||||||
let res = multiexp(&terms) * INV_EIGHT();
|
|
||||||
terms.zeroize();
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut sL = ScalarVector::new(padded_pow_of_2 * COMMITMENT_BITS);
|
|
||||||
let mut sR = ScalarVector::new(padded_pow_of_2 * COMMITMENT_BITS);
|
|
||||||
for i in 0 .. (padded_pow_of_2 * COMMITMENT_BITS) {
|
|
||||||
sL[i] = Scalar::random(&mut *rng);
|
|
||||||
sR[i] = Scalar::random(&mut *rng);
|
|
||||||
}
|
|
||||||
let rho = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let S = {
|
|
||||||
let mut terms = Vec::with_capacity(1 + (2 * sL.len()));
|
|
||||||
terms.push((rho, ED25519_BASEPOINT_POINT));
|
|
||||||
for (sL, G) in sL.0.iter().zip(&generators.G) {
|
|
||||||
terms.push((*sL, *G));
|
|
||||||
}
|
|
||||||
for (sR, H) in sR.0.iter().zip(&generators.H) {
|
|
||||||
terms.push((*sR, *H));
|
|
||||||
}
|
|
||||||
let res = multiexp(&terms) * INV_EIGHT();
|
|
||||||
terms.zeroize();
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
let (y, z) = Self::transcript_A_S(transcript, A, S);
|
|
||||||
transcript = z;
|
|
||||||
let z = ScalarVector::powers(z, 3 + padded_pow_of_2);
|
|
||||||
|
|
||||||
let twos = ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS);
|
|
||||||
|
|
||||||
let l = [aL - z[1], sL];
|
|
||||||
let y_pow_n = ScalarVector::powers(y, aR.len());
|
|
||||||
let mut r = [((aR + z[1]) * &y_pow_n), sR * &y_pow_n];
|
|
||||||
{
|
|
||||||
for j in 0 .. padded_pow_of_2 {
|
|
||||||
for i in 0 .. COMMITMENT_BITS {
|
|
||||||
r[0].0[(j * COMMITMENT_BITS) + i] += z[2 + j] * twos[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let t1 = (l[0].clone().inner_product(&r[1])) + (r[0].clone().inner_product(&l[1]));
|
|
||||||
let t2 = l[1].clone().inner_product(&r[1]);
|
|
||||||
|
|
||||||
let tau_1 = Scalar::random(&mut *rng);
|
|
||||||
let T1 = {
|
|
||||||
let mut T1_terms = [(t1, *MONERO_H), (tau_1, ED25519_BASEPOINT_POINT)];
|
|
||||||
for term in &mut T1_terms {
|
|
||||||
term.0 *= INV_EIGHT();
|
|
||||||
}
|
|
||||||
let T1 = multiexp(&T1_terms);
|
|
||||||
T1_terms.zeroize();
|
|
||||||
T1
|
|
||||||
};
|
|
||||||
let tau_2 = Scalar::random(&mut *rng);
|
|
||||||
let T2 = {
|
|
||||||
let mut T2_terms = [(t2, *MONERO_H), (tau_2, ED25519_BASEPOINT_POINT)];
|
|
||||||
for term in &mut T2_terms {
|
|
||||||
term.0 *= INV_EIGHT();
|
|
||||||
}
|
|
||||||
let T2 = multiexp(&T2_terms);
|
|
||||||
T2_terms.zeroize();
|
|
||||||
T2
|
|
||||||
};
|
|
||||||
|
|
||||||
transcript = Self::transcript_T12(transcript, T1, T2);
|
|
||||||
let x = transcript;
|
|
||||||
|
|
||||||
let [l0, l1] = l;
|
|
||||||
let l = l0 + &(l1 * x);
|
|
||||||
let [r0, r1] = r;
|
|
||||||
let r = r0 + &(r1 * x);
|
|
||||||
let t_hat = l.clone().inner_product(&r);
|
|
||||||
let mut tau_x = ((tau_2 * x) + tau_1) * x;
|
|
||||||
{
|
|
||||||
for (i, commitment) in witness.commitments.iter().enumerate() {
|
|
||||||
tau_x += z[2 + i] * commitment.mask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mu = alpha + (rho * x);
|
|
||||||
|
|
||||||
let y_inv_pow_n = ScalarVector::powers(y.invert(), l.len());
|
|
||||||
|
|
||||||
transcript = Self::transcript_tau_x_mu_t_hat(transcript, tau_x, mu, t_hat);
|
|
||||||
let x_ip = transcript;
|
|
||||||
|
|
||||||
let ip = IpStatement::new_without_P_transcript(y_inv_pow_n, x_ip)
|
|
||||||
.prove(
|
|
||||||
transcript,
|
|
||||||
IpWitness::new(l, r).expect("Bulletproofs::Original created an invalid IpWitness"),
|
|
||||||
)
|
|
||||||
.expect("Bulletproofs::Original failed to prove the inner-product");
|
|
||||||
|
|
||||||
let res = AggregateRangeProof { A, S, T1, T2, tau_x, mu, t_hat, ip };
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
let mut verifier = BulletproofsBatchVerifier::default();
|
|
||||||
debug_assert!(self.verify(rng, &mut verifier, res.clone()));
|
|
||||||
debug_assert!(verifier.verify());
|
|
||||||
}
|
|
||||||
Some(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn verify(
|
|
||||||
self,
|
|
||||||
rng: &mut (impl RngCore + CryptoRng),
|
|
||||||
verifier: &mut BulletproofsBatchVerifier,
|
|
||||||
mut proof: AggregateRangeProof,
|
|
||||||
) -> bool {
|
|
||||||
let mut padded_pow_of_2 = 1;
|
|
||||||
while padded_pow_of_2 < self.commitments.len() {
|
|
||||||
padded_pow_of_2 <<= 1;
|
|
||||||
}
|
|
||||||
let ip_rows = padded_pow_of_2 * COMMITMENT_BITS;
|
|
||||||
|
|
||||||
while verifier.0.g_bold.len() < ip_rows {
|
|
||||||
verifier.0.g_bold.push(Scalar::ZERO);
|
|
||||||
verifier.0.h_bold.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (mut transcript, mut commitments) = self.initial_transcript();
|
|
||||||
for commitment in &mut commitments {
|
|
||||||
*commitment = commitment.mul_by_cofactor();
|
|
||||||
}
|
|
||||||
|
|
||||||
let (y, z) = Self::transcript_A_S(transcript, proof.A, proof.S);
|
|
||||||
transcript = z;
|
|
||||||
let z = ScalarVector::powers(z, 3 + padded_pow_of_2);
|
|
||||||
transcript = Self::transcript_T12(transcript, proof.T1, proof.T2);
|
|
||||||
let x = transcript;
|
|
||||||
transcript = Self::transcript_tau_x_mu_t_hat(transcript, proof.tau_x, proof.mu, proof.t_hat);
|
|
||||||
let x_ip = transcript;
|
|
||||||
|
|
||||||
proof.A = proof.A.mul_by_cofactor();
|
|
||||||
proof.S = proof.S.mul_by_cofactor();
|
|
||||||
proof.T1 = proof.T1.mul_by_cofactor();
|
|
||||||
proof.T2 = proof.T2.mul_by_cofactor();
|
|
||||||
|
|
||||||
let y_pow_n = ScalarVector::powers(y, ip_rows);
|
|
||||||
let y_inv_pow_n = ScalarVector::powers(y.invert(), ip_rows);
|
|
||||||
|
|
||||||
let twos = ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS);
|
|
||||||
|
|
||||||
// 65
|
|
||||||
{
|
|
||||||
let weight = Scalar::random(&mut *rng);
|
|
||||||
verifier.0.h += weight * proof.t_hat;
|
|
||||||
verifier.0.g += weight * proof.tau_x;
|
|
||||||
|
|
||||||
// Now that we've accumulated the lhs, negate the weight and accumulate the rhs
|
|
||||||
// These will now sum to 0 if equal
|
|
||||||
let weight = -weight;
|
|
||||||
|
|
||||||
verifier.0.h += weight * (z[1] - (z[2])) * y_pow_n.sum();
|
|
||||||
|
|
||||||
for (i, commitment) in commitments.iter().enumerate() {
|
|
||||||
verifier.0.other.push((weight * z[2 + i], *commitment));
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0 .. padded_pow_of_2 {
|
|
||||||
verifier.0.h -= weight * z[3 + i] * twos.clone().sum();
|
|
||||||
}
|
|
||||||
verifier.0.other.push((weight * x, proof.T1));
|
|
||||||
verifier.0.other.push((weight * (x * x), proof.T2));
|
|
||||||
}
|
|
||||||
|
|
||||||
let ip_weight = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
// 66
|
|
||||||
verifier.0.other.push((ip_weight, proof.A));
|
|
||||||
verifier.0.other.push((ip_weight * x, proof.S));
|
|
||||||
// We can replace these with a g_sum, h_sum scalar in the batch verifier
|
|
||||||
// It'd trade `2 * ip_rows` scalar additions (per proof) for one scalar addition and an
|
|
||||||
// additional term in the MSM
|
|
||||||
let ip_z = ip_weight * z[1];
|
|
||||||
for i in 0 .. ip_rows {
|
|
||||||
verifier.0.h_bold[i] += ip_z;
|
|
||||||
}
|
|
||||||
let neg_ip_z = -ip_z;
|
|
||||||
for i in 0 .. ip_rows {
|
|
||||||
verifier.0.g_bold[i] += neg_ip_z;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
for j in 0 .. padded_pow_of_2 {
|
|
||||||
for i in 0 .. COMMITMENT_BITS {
|
|
||||||
let full_i = (j * COMMITMENT_BITS) + i;
|
|
||||||
verifier.0.h_bold[full_i] += ip_weight * y_inv_pow_n[full_i] * z[2 + j] * twos[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verifier.0.h += ip_weight * x_ip * proof.t_hat;
|
|
||||||
|
|
||||||
// 67, 68
|
|
||||||
verifier.0.g += ip_weight * -proof.mu;
|
|
||||||
let res = IpStatement::new_without_P_transcript(y_inv_pow_n, x_ip)
|
|
||||||
.verify(verifier, ip_rows, transcript, ip_weight, proof.ip);
|
|
||||||
res.is_ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
use std_shims::{vec, vec::Vec};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
|
||||||
|
|
||||||
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_primitives::{INV_EIGHT, Commitment, keccak256_to_scalar};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
batch_verifier::BulletproofsPlusBatchVerifier,
|
|
||||||
core::{MAX_COMMITMENTS, COMMITMENT_BITS, multiexp, multiexp_vartime},
|
|
||||||
plus::{
|
|
||||||
ScalarVector, PointVector, GeneratorsList, BpPlusGenerators,
|
|
||||||
transcript::*,
|
|
||||||
weighted_inner_product::{WipStatement, WipWitness, WipProof},
|
|
||||||
padded_pow_of_2, u64_decompose,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Figure 3 of the Bulletproofs+ Paper
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct AggregateRangeStatement<'a> {
|
|
||||||
generators: BpPlusGenerators,
|
|
||||||
V: &'a [EdwardsPoint],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct AggregateRangeWitness(Vec<Commitment>);
|
|
||||||
|
|
||||||
impl AggregateRangeWitness {
|
|
||||||
pub(crate) fn new(commitments: Vec<Commitment>) -> Option<Self> {
|
|
||||||
if commitments.is_empty() || (commitments.len() > MAX_COMMITMENTS) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(AggregateRangeWitness(commitments))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal structure representing a Bulletproof+, as defined by Monero..
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct AggregateRangeProof {
|
|
||||||
pub(crate) A: EdwardsPoint,
|
|
||||||
pub(crate) wip: WipProof,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AHatComputation {
|
|
||||||
y: Scalar,
|
|
||||||
d_descending_y_plus_z: ScalarVector,
|
|
||||||
y_mn_plus_one: Scalar,
|
|
||||||
z: Scalar,
|
|
||||||
z_pow: ScalarVector,
|
|
||||||
A_hat: EdwardsPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> AggregateRangeStatement<'a> {
|
|
||||||
pub(crate) fn new(V: &'a [EdwardsPoint]) -> Option<Self> {
|
|
||||||
if V.is_empty() || (V.len() > MAX_COMMITMENTS) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Self { generators: BpPlusGenerators::new(), V })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_A(transcript: &mut Scalar, A: EdwardsPoint) -> (Scalar, Scalar) {
|
|
||||||
let y = keccak256_to_scalar(
|
|
||||||
[transcript.to_bytes().as_ref(), A.compress().to_bytes().as_ref()].concat(),
|
|
||||||
);
|
|
||||||
let z = keccak256_to_scalar(y.to_bytes().as_ref());
|
|
||||||
*transcript = z;
|
|
||||||
(y, z)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn d_j(j: usize, m: usize) -> ScalarVector {
|
|
||||||
let mut d_j = Vec::with_capacity(m * COMMITMENT_BITS);
|
|
||||||
for _ in 0 .. (j - 1) * COMMITMENT_BITS {
|
|
||||||
d_j.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
d_j.append(&mut ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS).0);
|
|
||||||
for _ in 0 .. (m - j) * COMMITMENT_BITS {
|
|
||||||
d_j.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
ScalarVector(d_j)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_A_hat(
|
|
||||||
mut V: PointVector,
|
|
||||||
generators: &BpPlusGenerators,
|
|
||||||
transcript: &mut Scalar,
|
|
||||||
mut A: EdwardsPoint,
|
|
||||||
) -> AHatComputation {
|
|
||||||
let (y, z) = Self::transcript_A(transcript, A);
|
|
||||||
A = A.mul_by_cofactor();
|
|
||||||
|
|
||||||
while V.len() < padded_pow_of_2(V.len()) {
|
|
||||||
V.0.push(EdwardsPoint::identity());
|
|
||||||
}
|
|
||||||
let mn = V.len() * COMMITMENT_BITS;
|
|
||||||
|
|
||||||
// 2, 4, 6, 8... powers of z, of length equivalent to the amount of commitments
|
|
||||||
let mut z_pow = Vec::with_capacity(V.len());
|
|
||||||
// z**2
|
|
||||||
z_pow.push(z * z);
|
|
||||||
|
|
||||||
let mut d = ScalarVector::new(mn);
|
|
||||||
for j in 1 ..= V.len() {
|
|
||||||
z_pow.push(
|
|
||||||
*z_pow.last().expect("couldn't get last z_pow despite always being non-empty") * z_pow[0],
|
|
||||||
);
|
|
||||||
d = d + &(Self::d_j(j, V.len()) * (z_pow[j - 1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ascending_y = ScalarVector(vec![y]);
|
|
||||||
for i in 1 .. d.len() {
|
|
||||||
ascending_y.0.push(ascending_y[i - 1] * y);
|
|
||||||
}
|
|
||||||
let y_pows = ascending_y.clone().sum();
|
|
||||||
|
|
||||||
let mut descending_y = ascending_y.clone();
|
|
||||||
descending_y.0.reverse();
|
|
||||||
|
|
||||||
let d_descending_y = d.clone() * &descending_y;
|
|
||||||
let d_descending_y_plus_z = d_descending_y + z;
|
|
||||||
|
|
||||||
let y_mn_plus_one = descending_y[0] * y;
|
|
||||||
|
|
||||||
let mut commitment_accum = EdwardsPoint::identity();
|
|
||||||
for (j, commitment) in V.0.iter().enumerate() {
|
|
||||||
commitment_accum += *commitment * z_pow[j];
|
|
||||||
}
|
|
||||||
|
|
||||||
let neg_z = -z;
|
|
||||||
let mut A_terms = Vec::with_capacity((generators.len() * 2) + 2);
|
|
||||||
for (i, d_y_z) in d_descending_y_plus_z.0.iter().enumerate() {
|
|
||||||
A_terms.push((neg_z, generators.generator(GeneratorsList::GBold, i)));
|
|
||||||
A_terms.push((*d_y_z, generators.generator(GeneratorsList::HBold, i)));
|
|
||||||
}
|
|
||||||
A_terms.push((y_mn_plus_one, commitment_accum));
|
|
||||||
A_terms.push((
|
|
||||||
((y_pows * z) - (d.sum() * y_mn_plus_one * z) - (y_pows * (z * z))),
|
|
||||||
BpPlusGenerators::g(),
|
|
||||||
));
|
|
||||||
|
|
||||||
AHatComputation {
|
|
||||||
y,
|
|
||||||
d_descending_y_plus_z,
|
|
||||||
y_mn_plus_one,
|
|
||||||
z,
|
|
||||||
z_pow: ScalarVector(z_pow),
|
|
||||||
A_hat: A + multiexp_vartime(&A_terms),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
witness: &AggregateRangeWitness,
|
|
||||||
) -> Option<AggregateRangeProof> {
|
|
||||||
// Check for consistency with the witness
|
|
||||||
if self.V.len() != witness.0.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
for (commitment, witness) in self.V.iter().zip(witness.0.iter()) {
|
|
||||||
if witness.calculate() != *commitment {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Self { generators, V } = self;
|
|
||||||
// Monero expects all of these points to be torsion-free
|
|
||||||
// Generally, for Bulletproofs, it sends points * INV_EIGHT and then performs a torsion clear
|
|
||||||
// by multiplying by 8
|
|
||||||
// This also restores the original value due to the preprocessing
|
|
||||||
// Commitments aren't transmitted INV_EIGHT though, so this multiplies by INV_EIGHT to enable
|
|
||||||
// clearing its cofactor without mutating the value
|
|
||||||
// For some reason, these values are transcripted * INV_EIGHT, not as transmitted
|
|
||||||
let V = V.iter().map(|V| V * INV_EIGHT()).collect::<Vec<_>>();
|
|
||||||
let mut transcript = initial_transcript(V.iter());
|
|
||||||
let mut V = V.iter().map(EdwardsPoint::mul_by_cofactor).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Pad V
|
|
||||||
while V.len() < padded_pow_of_2(V.len()) {
|
|
||||||
V.push(EdwardsPoint::identity());
|
|
||||||
}
|
|
||||||
|
|
||||||
let generators = generators.reduce(V.len() * COMMITMENT_BITS);
|
|
||||||
|
|
||||||
let mut d_js = Vec::with_capacity(V.len());
|
|
||||||
let mut a_l = ScalarVector(Vec::with_capacity(V.len() * COMMITMENT_BITS));
|
|
||||||
for j in 1 ..= V.len() {
|
|
||||||
d_js.push(Self::d_j(j, V.len()));
|
|
||||||
#[allow(clippy::map_unwrap_or)]
|
|
||||||
a_l.0.append(
|
|
||||||
&mut u64_decompose(
|
|
||||||
*witness.0.get(j - 1).map(|commitment| &commitment.amount).unwrap_or(&0),
|
|
||||||
)
|
|
||||||
.0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let a_r = a_l.clone() - Scalar::ONE;
|
|
||||||
|
|
||||||
let alpha = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let mut A_terms = Vec::with_capacity((generators.len() * 2) + 1);
|
|
||||||
for (i, a_l) in a_l.0.iter().enumerate() {
|
|
||||||
A_terms.push((*a_l, generators.generator(GeneratorsList::GBold, i)));
|
|
||||||
}
|
|
||||||
for (i, a_r) in a_r.0.iter().enumerate() {
|
|
||||||
A_terms.push((*a_r, generators.generator(GeneratorsList::HBold, i)));
|
|
||||||
}
|
|
||||||
A_terms.push((alpha, BpPlusGenerators::h()));
|
|
||||||
let mut A = multiexp(&A_terms);
|
|
||||||
A_terms.zeroize();
|
|
||||||
|
|
||||||
// Multiply by INV_EIGHT per earlier commentary
|
|
||||||
A *= INV_EIGHT();
|
|
||||||
|
|
||||||
let AHatComputation { y, d_descending_y_plus_z, y_mn_plus_one, z, z_pow, A_hat } =
|
|
||||||
Self::compute_A_hat(PointVector(V), &generators, &mut transcript, A);
|
|
||||||
|
|
||||||
let a_l = a_l - z;
|
|
||||||
let a_r = a_r + &d_descending_y_plus_z;
|
|
||||||
let mut alpha = alpha;
|
|
||||||
for j in 1 ..= witness.0.len() {
|
|
||||||
alpha += z_pow[j - 1] * witness.0[j - 1].mask * y_mn_plus_one;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(AggregateRangeProof {
|
|
||||||
A,
|
|
||||||
wip: WipStatement::new(generators, A_hat, y)
|
|
||||||
.prove(
|
|
||||||
rng,
|
|
||||||
transcript,
|
|
||||||
&Zeroizing::new(
|
|
||||||
WipWitness::new(a_l, a_r, alpha)
|
|
||||||
.expect("Bulletproofs::Plus created an invalid WipWitness"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.expect("Bulletproof::Plus failed to prove the weighted inner-product"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn verify<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BulletproofsPlusBatchVerifier,
|
|
||||||
proof: AggregateRangeProof,
|
|
||||||
) -> bool {
|
|
||||||
let Self { generators, V } = self;
|
|
||||||
|
|
||||||
let V = V.iter().map(|V| V * INV_EIGHT()).collect::<Vec<_>>();
|
|
||||||
let mut transcript = initial_transcript(V.iter());
|
|
||||||
let V = V.iter().map(EdwardsPoint::mul_by_cofactor).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let generators = generators.reduce(V.len() * COMMITMENT_BITS);
|
|
||||||
|
|
||||||
let AHatComputation { y, A_hat, .. } =
|
|
||||||
Self::compute_A_hat(PointVector(V), &generators, &mut transcript, proof.A);
|
|
||||||
WipStatement::new(generators, A_hat, y).verify(rng, verifier, transcript, proof.wip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use std_shims::sync::LazyLock;
|
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_generators::{H, Generators};
|
|
||||||
|
|
||||||
pub(crate) use crate::{scalar_vector::ScalarVector, point_vector::PointVector};
|
|
||||||
|
|
||||||
pub(crate) mod transcript;
|
|
||||||
pub(crate) mod weighted_inner_product;
|
|
||||||
pub(crate) use weighted_inner_product::*;
|
|
||||||
pub(crate) mod aggregate_range_proof;
|
|
||||||
pub(crate) use aggregate_range_proof::*;
|
|
||||||
|
|
||||||
pub(crate) fn padded_pow_of_2(i: usize) -> usize {
|
|
||||||
let mut next_pow_of_2 = 1;
|
|
||||||
while next_pow_of_2 < i {
|
|
||||||
next_pow_of_2 <<= 1;
|
|
||||||
}
|
|
||||||
next_pow_of_2
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
||||||
pub(crate) enum GeneratorsList {
|
|
||||||
GBold,
|
|
||||||
HBold,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct BpPlusGenerators {
|
|
||||||
g_bold: &'static [EdwardsPoint],
|
|
||||||
h_bold: &'static [EdwardsPoint],
|
|
||||||
}
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/generators_plus.rs"));
|
|
||||||
|
|
||||||
impl BpPlusGenerators {
|
|
||||||
#[allow(clippy::new_without_default)]
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
let gens = &GENERATORS;
|
|
||||||
BpPlusGenerators { g_bold: &gens.G, h_bold: &gens.H }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.g_bold.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn g() -> EdwardsPoint {
|
|
||||||
*H
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn h() -> EdwardsPoint {
|
|
||||||
ED25519_BASEPOINT_POINT
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn generator(&self, list: GeneratorsList, i: usize) -> EdwardsPoint {
|
|
||||||
match list {
|
|
||||||
GeneratorsList::GBold => self.g_bold[i],
|
|
||||||
GeneratorsList::HBold => self.h_bold[i],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn reduce(&self, generators: usize) -> Self {
|
|
||||||
// Round to the nearest power of 2
|
|
||||||
let generators = padded_pow_of_2(generators);
|
|
||||||
assert!(
|
|
||||||
generators <= self.g_bold.len(),
|
|
||||||
"instantiated with less generators than application required"
|
|
||||||
);
|
|
||||||
|
|
||||||
BpPlusGenerators { g_bold: &self.g_bold[.. generators], h_bold: &self.h_bold[.. generators] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the little-endian decomposition.
|
|
||||||
fn u64_decompose(value: u64) -> ScalarVector {
|
|
||||||
let mut bits = ScalarVector::new(64);
|
|
||||||
for bit in 0 .. 64 {
|
|
||||||
bits[bit] = Scalar::from((value >> bit) & 1);
|
|
||||||
}
|
|
||||||
bits
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
use std_shims::{sync::LazyLock, vec::Vec};
|
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_generators::hash_to_point;
|
|
||||||
use monero_primitives::{keccak256, keccak256_to_scalar};
|
|
||||||
|
|
||||||
// Monero starts BP+ transcripts with the following constant.
|
|
||||||
// Why this uses a hash_to_point is completely unknown.
|
|
||||||
pub(crate) static TRANSCRIPT: LazyLock<[u8; 32]> =
|
|
||||||
LazyLock::new(|| hash_to_point(keccak256(b"bulletproof_plus_transcript")).compress().to_bytes());
|
|
||||||
|
|
||||||
pub(crate) fn initial_transcript(commitments: core::slice::Iter<'_, EdwardsPoint>) -> Scalar {
|
|
||||||
let commitments_hash =
|
|
||||||
keccak256_to_scalar(commitments.flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>());
|
|
||||||
keccak256_to_scalar([TRANSCRIPT.as_ref(), &commitments_hash.to_bytes()].concat())
|
|
||||||
}
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
use std_shims::{vec, vec::Vec};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_primitives::{INV_EIGHT, keccak256_to_scalar};
|
|
||||||
use crate::{
|
|
||||||
core::{multiexp, multiexp_vartime, challenge_products},
|
|
||||||
batch_verifier::BulletproofsPlusBatchVerifier,
|
|
||||||
plus::{ScalarVector, PointVector, GeneratorsList, BpPlusGenerators, padded_pow_of_2},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Figure 1 of the Bulletproofs+ paper
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct WipStatement {
|
|
||||||
generators: BpPlusGenerators,
|
|
||||||
P: EdwardsPoint,
|
|
||||||
y: ScalarVector,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Zeroize for WipStatement {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.P.zeroize();
|
|
||||||
self.y.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct WipWitness {
|
|
||||||
a: ScalarVector,
|
|
||||||
b: ScalarVector,
|
|
||||||
alpha: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WipWitness {
|
|
||||||
pub(crate) fn new(mut a: ScalarVector, mut b: ScalarVector, alpha: Scalar) -> Option<Self> {
|
|
||||||
if a.0.is_empty() || (a.len() != b.len()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pad to the nearest power of 2
|
|
||||||
let missing = padded_pow_of_2(a.len()) - a.len();
|
|
||||||
a.0.reserve(missing);
|
|
||||||
b.0.reserve(missing);
|
|
||||||
for _ in 0 .. missing {
|
|
||||||
a.0.push(Scalar::ZERO);
|
|
||||||
b.0.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Self { a, b, alpha })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub(crate) struct WipProof {
|
|
||||||
pub(crate) L: Vec<EdwardsPoint>,
|
|
||||||
pub(crate) R: Vec<EdwardsPoint>,
|
|
||||||
pub(crate) A: EdwardsPoint,
|
|
||||||
pub(crate) B: EdwardsPoint,
|
|
||||||
pub(crate) r_answer: Scalar,
|
|
||||||
pub(crate) s_answer: Scalar,
|
|
||||||
pub(crate) delta_answer: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WipStatement {
|
|
||||||
pub(crate) fn new(generators: BpPlusGenerators, P: EdwardsPoint, y: Scalar) -> Self {
|
|
||||||
debug_assert_eq!(generators.len(), padded_pow_of_2(generators.len()));
|
|
||||||
|
|
||||||
// y ** n
|
|
||||||
let mut y_vec = ScalarVector::new(generators.len());
|
|
||||||
y_vec[0] = y;
|
|
||||||
for i in 1 .. y_vec.len() {
|
|
||||||
y_vec[i] = y_vec[i - 1] * y;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self { generators, P, y: y_vec }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_L_R(transcript: &mut Scalar, L: EdwardsPoint, R: EdwardsPoint) -> Scalar {
|
|
||||||
let e = keccak256_to_scalar(
|
|
||||||
[
|
|
||||||
transcript.to_bytes().as_ref(),
|
|
||||||
L.compress().to_bytes().as_ref(),
|
|
||||||
R.compress().to_bytes().as_ref(),
|
|
||||||
]
|
|
||||||
.concat(),
|
|
||||||
);
|
|
||||||
*transcript = e;
|
|
||||||
e
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_A_B(transcript: &mut Scalar, A: EdwardsPoint, B: EdwardsPoint) -> Scalar {
|
|
||||||
let e = keccak256_to_scalar(
|
|
||||||
[
|
|
||||||
transcript.to_bytes().as_ref(),
|
|
||||||
A.compress().to_bytes().as_ref(),
|
|
||||||
B.compress().to_bytes().as_ref(),
|
|
||||||
]
|
|
||||||
.concat(),
|
|
||||||
);
|
|
||||||
*transcript = e;
|
|
||||||
e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prover's variant of the shared code block to calculate G/H/P when n > 1
|
|
||||||
// Returns each permutation of G/H since the prover needs to do operation on each permutation
|
|
||||||
// P is dropped as it's unused in the prover's path
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn next_G_H(
|
|
||||||
transcript: &mut Scalar,
|
|
||||||
mut g_bold1: PointVector,
|
|
||||||
mut g_bold2: PointVector,
|
|
||||||
mut h_bold1: PointVector,
|
|
||||||
mut h_bold2: PointVector,
|
|
||||||
L: EdwardsPoint,
|
|
||||||
R: EdwardsPoint,
|
|
||||||
y_inv_n_hat: Scalar,
|
|
||||||
) -> (Scalar, Scalar, Scalar, Scalar, PointVector, PointVector) {
|
|
||||||
debug_assert_eq!(g_bold1.len(), g_bold2.len());
|
|
||||||
debug_assert_eq!(h_bold1.len(), h_bold2.len());
|
|
||||||
debug_assert_eq!(g_bold1.len(), h_bold1.len());
|
|
||||||
|
|
||||||
let e = Self::transcript_L_R(transcript, L, R);
|
|
||||||
let inv_e = e.invert();
|
|
||||||
|
|
||||||
// This vartime is safe as all of these arguments are public
|
|
||||||
let mut new_g_bold = Vec::with_capacity(g_bold1.len());
|
|
||||||
let e_y_inv = e * y_inv_n_hat;
|
|
||||||
for g_bold in g_bold1.0.drain(..).zip(g_bold2.0.drain(..)) {
|
|
||||||
new_g_bold.push(multiexp_vartime(&[(inv_e, g_bold.0), (e_y_inv, g_bold.1)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut new_h_bold = Vec::with_capacity(h_bold1.len());
|
|
||||||
for h_bold in h_bold1.0.drain(..).zip(h_bold2.0.drain(..)) {
|
|
||||||
new_h_bold.push(multiexp_vartime(&[(e, h_bold.0), (inv_e, h_bold.1)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
let e_square = e * e;
|
|
||||||
let inv_e_square = inv_e * inv_e;
|
|
||||||
|
|
||||||
(e, inv_e, e_square, inv_e_square, PointVector(new_g_bold), PointVector(new_h_bold))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
mut transcript: Scalar,
|
|
||||||
witness: &WipWitness,
|
|
||||||
) -> Option<WipProof> {
|
|
||||||
let WipStatement { generators, P, mut y } = self;
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let _ = P;
|
|
||||||
|
|
||||||
if generators.len() != witness.a.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (g, h) = (BpPlusGenerators::g(), BpPlusGenerators::h());
|
|
||||||
let mut g_bold = vec![];
|
|
||||||
let mut h_bold = vec![];
|
|
||||||
for i in 0 .. generators.len() {
|
|
||||||
g_bold.push(generators.generator(GeneratorsList::GBold, i));
|
|
||||||
h_bold.push(generators.generator(GeneratorsList::HBold, i));
|
|
||||||
}
|
|
||||||
let mut g_bold = PointVector(g_bold);
|
|
||||||
let mut h_bold = PointVector(h_bold);
|
|
||||||
|
|
||||||
let mut y_inv = {
|
|
||||||
let mut i = 1;
|
|
||||||
let mut to_invert = vec![];
|
|
||||||
while i < g_bold.len() {
|
|
||||||
to_invert.push(y[i - 1]);
|
|
||||||
i *= 2;
|
|
||||||
}
|
|
||||||
Scalar::batch_invert(&mut to_invert);
|
|
||||||
to_invert
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check P has the expected relationship
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
let mut P_terms = witness
|
|
||||||
.a
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.zip(g_bold.0.iter().copied())
|
|
||||||
.chain(witness.b.0.iter().copied().zip(h_bold.0.iter().copied()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
P_terms.push((witness.a.clone().weighted_inner_product(&witness.b, &y), g));
|
|
||||||
P_terms.push((witness.alpha, h));
|
|
||||||
debug_assert_eq!(multiexp(&P_terms), P);
|
|
||||||
P_terms.zeroize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut a = witness.a.clone();
|
|
||||||
let mut b = witness.b.clone();
|
|
||||||
let mut alpha = witness.alpha;
|
|
||||||
|
|
||||||
// From here on, g_bold.len() is used as n
|
|
||||||
debug_assert_eq!(g_bold.len(), a.len());
|
|
||||||
|
|
||||||
let mut L_vec = vec![];
|
|
||||||
let mut R_vec = vec![];
|
|
||||||
|
|
||||||
// else n > 1 case from figure 1
|
|
||||||
while g_bold.len() > 1 {
|
|
||||||
let (a1, a2) = a.clone().split();
|
|
||||||
let (b1, b2) = b.clone().split();
|
|
||||||
let (g_bold1, g_bold2) = g_bold.split();
|
|
||||||
let (h_bold1, h_bold2) = h_bold.split();
|
|
||||||
|
|
||||||
let n_hat = g_bold1.len();
|
|
||||||
debug_assert_eq!(a1.len(), n_hat);
|
|
||||||
debug_assert_eq!(a2.len(), n_hat);
|
|
||||||
debug_assert_eq!(b1.len(), n_hat);
|
|
||||||
debug_assert_eq!(b2.len(), n_hat);
|
|
||||||
debug_assert_eq!(g_bold1.len(), n_hat);
|
|
||||||
debug_assert_eq!(g_bold2.len(), n_hat);
|
|
||||||
debug_assert_eq!(h_bold1.len(), n_hat);
|
|
||||||
debug_assert_eq!(h_bold2.len(), n_hat);
|
|
||||||
|
|
||||||
let y_n_hat = y[n_hat - 1];
|
|
||||||
y.0.truncate(n_hat);
|
|
||||||
|
|
||||||
let d_l = Scalar::random(&mut *rng);
|
|
||||||
let d_r = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let c_l = a1.clone().weighted_inner_product(&b2, &y);
|
|
||||||
let c_r = (a2.clone() * y_n_hat).weighted_inner_product(&b1, &y);
|
|
||||||
|
|
||||||
let y_inv_n_hat = y_inv
|
|
||||||
.pop()
|
|
||||||
.expect("couldn't pop y_inv despite y_inv being of same length as times iterated");
|
|
||||||
|
|
||||||
let mut L_terms = (a1.clone() * y_inv_n_hat)
|
|
||||||
.0
|
|
||||||
.drain(..)
|
|
||||||
.zip(g_bold2.0.iter().copied())
|
|
||||||
.chain(b2.0.iter().copied().zip(h_bold1.0.iter().copied()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
L_terms.push((c_l, g));
|
|
||||||
L_terms.push((d_l, h));
|
|
||||||
let L = multiexp(&L_terms) * INV_EIGHT();
|
|
||||||
L_vec.push(L);
|
|
||||||
L_terms.zeroize();
|
|
||||||
|
|
||||||
let mut R_terms = (a2.clone() * y_n_hat)
|
|
||||||
.0
|
|
||||||
.drain(..)
|
|
||||||
.zip(g_bold1.0.iter().copied())
|
|
||||||
.chain(b1.0.iter().copied().zip(h_bold2.0.iter().copied()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
R_terms.push((c_r, g));
|
|
||||||
R_terms.push((d_r, h));
|
|
||||||
let R = multiexp(&R_terms) * INV_EIGHT();
|
|
||||||
R_vec.push(R);
|
|
||||||
R_terms.zeroize();
|
|
||||||
|
|
||||||
let (e, inv_e, e_square, inv_e_square);
|
|
||||||
(e, inv_e, e_square, inv_e_square, g_bold, h_bold) =
|
|
||||||
Self::next_G_H(&mut transcript, g_bold1, g_bold2, h_bold1, h_bold2, L, R, y_inv_n_hat);
|
|
||||||
|
|
||||||
a = (a1 * e) + &(a2 * (y_n_hat * inv_e));
|
|
||||||
b = (b1 * inv_e) + &(b2 * e);
|
|
||||||
alpha += (d_l * e_square) + (d_r * inv_e_square);
|
|
||||||
|
|
||||||
debug_assert_eq!(g_bold.len(), a.len());
|
|
||||||
debug_assert_eq!(g_bold.len(), h_bold.len());
|
|
||||||
debug_assert_eq!(g_bold.len(), b.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
// n == 1 case from figure 1
|
|
||||||
debug_assert_eq!(g_bold.len(), 1);
|
|
||||||
debug_assert_eq!(h_bold.len(), 1);
|
|
||||||
|
|
||||||
debug_assert_eq!(a.len(), 1);
|
|
||||||
debug_assert_eq!(b.len(), 1);
|
|
||||||
|
|
||||||
let r = Scalar::random(&mut *rng);
|
|
||||||
let s = Scalar::random(&mut *rng);
|
|
||||||
let delta = Scalar::random(&mut *rng);
|
|
||||||
let eta = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let ry = r * y[0];
|
|
||||||
|
|
||||||
let mut A_terms =
|
|
||||||
vec![(r, g_bold[0]), (s, h_bold[0]), ((ry * b[0]) + (s * y[0] * a[0]), g), (delta, h)];
|
|
||||||
let A = multiexp(&A_terms) * INV_EIGHT();
|
|
||||||
A_terms.zeroize();
|
|
||||||
|
|
||||||
let mut B_terms = vec![(ry * s, g), (eta, h)];
|
|
||||||
let B = multiexp(&B_terms) * INV_EIGHT();
|
|
||||||
B_terms.zeroize();
|
|
||||||
|
|
||||||
let e = Self::transcript_A_B(&mut transcript, A, B);
|
|
||||||
|
|
||||||
let r_answer = r + (a[0] * e);
|
|
||||||
let s_answer = s + (b[0] * e);
|
|
||||||
let delta_answer = eta + (delta * e) + (alpha * (e * e));
|
|
||||||
|
|
||||||
Some(WipProof { L: L_vec, R: R_vec, A, B, r_answer, s_answer, delta_answer })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn verify<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BulletproofsPlusBatchVerifier,
|
|
||||||
mut transcript: Scalar,
|
|
||||||
mut proof: WipProof,
|
|
||||||
) -> bool {
|
|
||||||
let verifier_weight = Scalar::random(rng);
|
|
||||||
|
|
||||||
let WipStatement { generators, P, y } = self;
|
|
||||||
|
|
||||||
// Verify the L/R lengths
|
|
||||||
{
|
|
||||||
let mut lr_len = 0;
|
|
||||||
while (1 << lr_len) < generators.len() {
|
|
||||||
lr_len += 1;
|
|
||||||
}
|
|
||||||
if (proof.L.len() != lr_len) ||
|
|
||||||
(proof.R.len() != lr_len) ||
|
|
||||||
(generators.len() != (1 << lr_len))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inv_y = {
|
|
||||||
let inv_y = y[0].invert();
|
|
||||||
let mut res = Vec::with_capacity(y.len());
|
|
||||||
res.push(inv_y);
|
|
||||||
while res.len() < y.len() {
|
|
||||||
res.push(
|
|
||||||
inv_y * res.last().expect("couldn't get last inv_y despite inv_y always being non-empty"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut e_is = Vec::with_capacity(proof.L.len());
|
|
||||||
for (L, R) in proof.L.iter_mut().zip(proof.R.iter_mut()) {
|
|
||||||
e_is.push(Self::transcript_L_R(&mut transcript, *L, *R));
|
|
||||||
*L = L.mul_by_cofactor();
|
|
||||||
*R = R.mul_by_cofactor();
|
|
||||||
}
|
|
||||||
|
|
||||||
let e = Self::transcript_A_B(&mut transcript, proof.A, proof.B);
|
|
||||||
proof.A = proof.A.mul_by_cofactor();
|
|
||||||
proof.B = proof.B.mul_by_cofactor();
|
|
||||||
let neg_e_square = verifier_weight * -(e * e);
|
|
||||||
|
|
||||||
verifier.0.other.push((neg_e_square, P));
|
|
||||||
|
|
||||||
let mut challenges = Vec::with_capacity(proof.L.len());
|
|
||||||
let product_cache = {
|
|
||||||
let mut inv_e_is = e_is.clone();
|
|
||||||
Scalar::batch_invert(&mut inv_e_is);
|
|
||||||
|
|
||||||
debug_assert_eq!(e_is.len(), inv_e_is.len());
|
|
||||||
debug_assert_eq!(e_is.len(), proof.L.len());
|
|
||||||
debug_assert_eq!(e_is.len(), proof.R.len());
|
|
||||||
for ((e_i, inv_e_i), (L, R)) in
|
|
||||||
e_is.drain(..).zip(inv_e_is.drain(..)).zip(proof.L.iter().zip(proof.R.iter()))
|
|
||||||
{
|
|
||||||
debug_assert_eq!(e_i.invert(), inv_e_i);
|
|
||||||
|
|
||||||
challenges.push((e_i, inv_e_i));
|
|
||||||
|
|
||||||
let e_i_square = e_i * e_i;
|
|
||||||
let inv_e_i_square = inv_e_i * inv_e_i;
|
|
||||||
verifier.0.other.push((neg_e_square * e_i_square, *L));
|
|
||||||
verifier.0.other.push((neg_e_square * inv_e_i_square, *R));
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge_products(&challenges)
|
|
||||||
};
|
|
||||||
|
|
||||||
while verifier.0.g_bold.len() < generators.len() {
|
|
||||||
verifier.0.g_bold.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
while verifier.0.h_bold.len() < generators.len() {
|
|
||||||
verifier.0.h_bold.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
let re = proof.r_answer * e;
|
|
||||||
for i in 0 .. generators.len() {
|
|
||||||
let mut scalar = product_cache[i] * re;
|
|
||||||
if i > 0 {
|
|
||||||
scalar *= inv_y[i - 1];
|
|
||||||
}
|
|
||||||
verifier.0.g_bold[i] += verifier_weight * scalar;
|
|
||||||
}
|
|
||||||
|
|
||||||
let se = proof.s_answer * e;
|
|
||||||
for i in 0 .. generators.len() {
|
|
||||||
verifier.0.h_bold[i] += verifier_weight * (se * product_cache[product_cache.len() - 1 - i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
verifier.0.other.push((verifier_weight * -e, proof.A));
|
|
||||||
verifier.0.g += verifier_weight * (proof.r_answer * y[0] * proof.s_answer);
|
|
||||||
verifier.0.h += verifier_weight * proof.delta_answer;
|
|
||||||
verifier.0.other.push((-verifier_weight, proof.B));
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
use core::ops::{Index, IndexMut};
|
|
||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
|
||||||
|
|
||||||
use crate::scalar_vector::ScalarVector;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use crate::core::multiexp;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub(crate) struct PointVector(pub(crate) Vec<EdwardsPoint>);
|
|
||||||
|
|
||||||
impl Index<usize> for PointVector {
|
|
||||||
type Output = EdwardsPoint;
|
|
||||||
fn index(&self, index: usize) -> &EdwardsPoint {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IndexMut<usize> for PointVector {
|
|
||||||
fn index_mut(&mut self, index: usize) -> &mut EdwardsPoint {
|
|
||||||
&mut self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PointVector {
|
|
||||||
pub(crate) fn mul_vec(&self, vector: &ScalarVector) -> Self {
|
|
||||||
assert_eq!(self.len(), vector.len());
|
|
||||||
let mut res = self.clone();
|
|
||||||
for (i, val) in res.0.iter_mut().enumerate() {
|
|
||||||
*val *= vector.0[i];
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn multiexp(&self, vector: &ScalarVector) -> EdwardsPoint {
|
|
||||||
debug_assert_eq!(self.len(), vector.len());
|
|
||||||
let mut res = Vec::with_capacity(self.len());
|
|
||||||
for (point, scalar) in self.0.iter().copied().zip(vector.0.iter().copied()) {
|
|
||||||
res.push((scalar, point));
|
|
||||||
}
|
|
||||||
multiexp(&res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn split(mut self) -> (Self, Self) {
|
|
||||||
debug_assert!(self.len() > 1);
|
|
||||||
let r = self.0.split_off(self.0.len() / 2);
|
|
||||||
debug_assert_eq!(self.len(), r.len());
|
|
||||||
(self, PointVector(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
use core::{
|
|
||||||
borrow::Borrow,
|
|
||||||
ops::{Index, IndexMut, Add, Sub, Mul},
|
|
||||||
};
|
|
||||||
use std_shims::{vec, vec::Vec};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use crate::core::multiexp;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct ScalarVector(pub(crate) Vec<Scalar>);
|
|
||||||
|
|
||||||
impl Index<usize> for ScalarVector {
|
|
||||||
type Output = Scalar;
|
|
||||||
fn index(&self, index: usize) -> &Scalar {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl IndexMut<usize> for ScalarVector {
|
|
||||||
fn index_mut(&mut self, index: usize) -> &mut Scalar {
|
|
||||||
&mut self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: Borrow<Scalar>> Add<S> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn add(mut self, scalar: S) -> ScalarVector {
|
|
||||||
for s in &mut self.0 {
|
|
||||||
*s += scalar.borrow();
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<S: Borrow<Scalar>> Sub<S> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn sub(mut self, scalar: S) -> ScalarVector {
|
|
||||||
for s in &mut self.0 {
|
|
||||||
*s -= scalar.borrow();
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<S: Borrow<Scalar>> Mul<S> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn mul(mut self, scalar: S) -> ScalarVector {
|
|
||||||
for s in &mut self.0 {
|
|
||||||
*s *= scalar.borrow();
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Add<&ScalarVector> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn add(mut self, other: &ScalarVector) -> ScalarVector {
|
|
||||||
debug_assert_eq!(self.len(), other.len());
|
|
||||||
for (s, o) in self.0.iter_mut().zip(other.0.iter()) {
|
|
||||||
*s += o;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Sub<&ScalarVector> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn sub(mut self, other: &ScalarVector) -> ScalarVector {
|
|
||||||
debug_assert_eq!(self.len(), other.len());
|
|
||||||
for (s, o) in self.0.iter_mut().zip(other.0.iter()) {
|
|
||||||
*s -= o;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Mul<&ScalarVector> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn mul(mut self, other: &ScalarVector) -> ScalarVector {
|
|
||||||
debug_assert_eq!(self.len(), other.len());
|
|
||||||
for (s, o) in self.0.iter_mut().zip(other.0.iter()) {
|
|
||||||
*s *= o;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mul<&[EdwardsPoint]> for &ScalarVector {
|
|
||||||
type Output = EdwardsPoint;
|
|
||||||
fn mul(self, b: &[EdwardsPoint]) -> EdwardsPoint {
|
|
||||||
debug_assert_eq!(self.len(), b.len());
|
|
||||||
let mut multiexp_args = self.0.iter().copied().zip(b.iter().copied()).collect::<Vec<_>>();
|
|
||||||
let res = multiexp(&multiexp_args);
|
|
||||||
multiexp_args.zeroize();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScalarVector {
|
|
||||||
pub(crate) fn new(len: usize) -> Self {
|
|
||||||
ScalarVector(vec![Scalar::ZERO; len])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn powers(x: Scalar, len: usize) -> Self {
|
|
||||||
debug_assert!(len != 0);
|
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(len);
|
|
||||||
res.push(Scalar::ONE);
|
|
||||||
res.push(x);
|
|
||||||
for i in 2 .. len {
|
|
||||||
res.push(res[i - 1] * x);
|
|
||||||
}
|
|
||||||
res.truncate(len);
|
|
||||||
ScalarVector(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sum(mut self) -> Scalar {
|
|
||||||
self.0.drain(..).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn inner_product(self, vector: &Self) -> Scalar {
|
|
||||||
(self * vector).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn weighted_inner_product(self, vector: &Self, y: &Self) -> Scalar {
|
|
||||||
(self * vector * y).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn split(mut self) -> (Self, Self) {
|
|
||||||
debug_assert!(self.len() > 1);
|
|
||||||
let r = self.0.split_off(self.0.len() / 2);
|
|
||||||
debug_assert_eq!(self.len(), r.len());
|
|
||||||
(self, ScalarVector(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
|
|
||||||
use curve25519_dalek::scalar::Scalar;
|
|
||||||
|
|
||||||
use monero_primitives::Commitment;
|
|
||||||
use crate::{batch_verifier::BatchVerifier, Bulletproof, BulletproofError};
|
|
||||||
|
|
||||||
mod original;
|
|
||||||
mod plus;
|
|
||||||
|
|
||||||
macro_rules! bulletproofs_tests {
|
|
||||||
($name: ident, $max: ident, $plus: literal) => {
|
|
||||||
#[test]
|
|
||||||
fn $name() {
|
|
||||||
// Create Bulletproofs for all possible output quantities
|
|
||||||
let mut verifier = BatchVerifier::new();
|
|
||||||
for i in 1 ..= 16 {
|
|
||||||
let commitments = (1 ..= i)
|
|
||||||
.map(|_| Commitment::new(Scalar::random(&mut OsRng), OsRng.next_u64()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let bp = if $plus {
|
|
||||||
Bulletproof::prove_plus(&mut OsRng, commitments.clone()).unwrap()
|
|
||||||
} else {
|
|
||||||
Bulletproof::prove(&mut OsRng, commitments.clone()).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let commitments = commitments.iter().map(Commitment::calculate).collect::<Vec<_>>();
|
|
||||||
assert!(bp.verify(&mut OsRng, &commitments));
|
|
||||||
assert!(bp.batch_verify(&mut OsRng, &mut verifier, &commitments));
|
|
||||||
}
|
|
||||||
assert!(verifier.verify());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn $max() {
|
|
||||||
// Check Bulletproofs errors if we try to prove for too many outputs
|
|
||||||
let mut commitments = vec![];
|
|
||||||
for _ in 0 .. 17 {
|
|
||||||
commitments.push(Commitment::new(Scalar::ZERO, 0));
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
(if $plus {
|
|
||||||
Bulletproof::prove_plus(&mut OsRng, commitments)
|
|
||||||
} else {
|
|
||||||
Bulletproof::prove(&mut OsRng, commitments)
|
|
||||||
})
|
|
||||||
.unwrap_err(),
|
|
||||||
BulletproofError::TooManyCommitments,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bulletproofs_tests!(bulletproofs, bulletproofs_max, false);
|
|
||||||
bulletproofs_tests!(bulletproofs_plus, bulletproofs_plus_max, true);
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// The inner product relation is P = sum(g_bold * a, h_bold * b, g * (a * b))
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use curve25519_dalek::Scalar;
|
|
||||||
|
|
||||||
use monero_generators::H;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
scalar_vector::ScalarVector,
|
|
||||||
point_vector::PointVector,
|
|
||||||
original::{
|
|
||||||
GENERATORS,
|
|
||||||
inner_product::{IpStatement, IpWitness},
|
|
||||||
},
|
|
||||||
BulletproofsBatchVerifier,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_zero_inner_product() {
|
|
||||||
let statement =
|
|
||||||
IpStatement::new_without_P_transcript(ScalarVector(vec![Scalar::ONE; 1]), Scalar::ONE);
|
|
||||||
let witness = IpWitness::new(ScalarVector::new(1), ScalarVector::new(1)).unwrap();
|
|
||||||
|
|
||||||
let transcript = Scalar::random(&mut OsRng);
|
|
||||||
let proof = statement.clone().prove(transcript, witness).unwrap();
|
|
||||||
|
|
||||||
let mut verifier = BulletproofsBatchVerifier::default();
|
|
||||||
verifier.0.g_bold = vec![Scalar::ZERO; 1];
|
|
||||||
verifier.0.h_bold = vec![Scalar::ZERO; 1];
|
|
||||||
statement.verify(&mut verifier, 1, transcript, Scalar::random(&mut OsRng), proof).unwrap();
|
|
||||||
assert!(verifier.verify());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_inner_product() {
|
|
||||||
// P = sum(g_bold * a, h_bold * b, g * u * <a, b>)
|
|
||||||
let generators = &GENERATORS;
|
|
||||||
let mut verifier = BulletproofsBatchVerifier::default();
|
|
||||||
verifier.0.g_bold = vec![Scalar::ZERO; 32];
|
|
||||||
verifier.0.h_bold = vec![Scalar::ZERO; 32];
|
|
||||||
for i in [1, 2, 4, 8, 16, 32] {
|
|
||||||
let g = *H;
|
|
||||||
let mut g_bold = vec![];
|
|
||||||
let mut h_bold = vec![];
|
|
||||||
for i in 0 .. i {
|
|
||||||
g_bold.push(generators.G[i]);
|
|
||||||
h_bold.push(generators.H[i]);
|
|
||||||
}
|
|
||||||
let g_bold = PointVector(g_bold);
|
|
||||||
let h_bold = PointVector(h_bold);
|
|
||||||
|
|
||||||
let mut a = ScalarVector::new(i);
|
|
||||||
let mut b = ScalarVector::new(i);
|
|
||||||
|
|
||||||
for i in 0 .. i {
|
|
||||||
a[i] = Scalar::random(&mut OsRng);
|
|
||||||
b[i] = Scalar::random(&mut OsRng);
|
|
||||||
}
|
|
||||||
|
|
||||||
let P = g_bold.multiexp(&a) + h_bold.multiexp(&b) + (g * a.clone().inner_product(&b));
|
|
||||||
|
|
||||||
let statement =
|
|
||||||
IpStatement::new_without_P_transcript(ScalarVector(vec![Scalar::ONE; i]), Scalar::ONE);
|
|
||||||
let witness = IpWitness::new(a, b).unwrap();
|
|
||||||
|
|
||||||
let transcript = Scalar::random(&mut OsRng);
|
|
||||||
let proof = statement.clone().prove(transcript, witness).unwrap();
|
|
||||||
|
|
||||||
let weight = Scalar::random(&mut OsRng);
|
|
||||||
verifier.0.other.push((weight, P));
|
|
||||||
statement.verify(&mut verifier, i, transcript, weight, proof).unwrap();
|
|
||||||
}
|
|
||||||
assert!(verifier.verify());
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
use hex_literal::hex;
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use curve25519_dalek::scalar::Scalar;
|
|
||||||
|
|
||||||
use monero_io::decompress_point;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
original::{IpProof, AggregateRangeProof as OriginalProof},
|
|
||||||
Bulletproof,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod inner_product;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bulletproofs_vector() {
|
|
||||||
let scalar = |scalar| Scalar::from_canonical_bytes(scalar).unwrap();
|
|
||||||
let point = |point| decompress_point(point).unwrap();
|
|
||||||
|
|
||||||
// Generated from Monero
|
|
||||||
assert!(Bulletproof::Original(OriginalProof {
|
|
||||||
A: point(hex!("ef32c0b9551b804decdcb107eb22aa715b7ce259bf3c5cac20e24dfa6b28ac71")),
|
|
||||||
S: point(hex!("e1285960861783574ee2b689ae53622834eb0b035d6943103f960cd23e063fa0")),
|
|
||||||
T1: point(hex!("4ea07735f184ba159d0e0eb662bac8cde3eb7d39f31e567b0fbda3aa23fe5620")),
|
|
||||||
T2: point(hex!("b8390aa4b60b255630d40e592f55ec6b7ab5e3a96bfcdcd6f1cd1d2fc95f441e")),
|
|
||||||
tau_x: scalar(hex!("5957dba8ea9afb23d6e81cc048a92f2d502c10c749dc1b2bd148ae8d41ec7107")),
|
|
||||||
mu: scalar(hex!("923023b234c2e64774b820b4961f7181f6c1dc152c438643e5a25b0bf271bc02")),
|
|
||||||
ip: IpProof {
|
|
||||||
L: vec![
|
|
||||||
point(hex!("c45f656316b9ebf9d357fb6a9f85b5f09e0b991dd50a6e0ae9b02de3946c9d99")),
|
|
||||||
point(hex!("9304d2bf0f27183a2acc58cc755a0348da11bd345485fda41b872fee89e72aac")),
|
|
||||||
point(hex!("1bb8b71925d155dd9569f64129ea049d6149fdc4e7a42a86d9478801d922129b")),
|
|
||||||
point(hex!("5756a7bf887aa72b9a952f92f47182122e7b19d89e5dd434c747492b00e1c6b7")),
|
|
||||||
point(hex!("6e497c910d102592830555356af5ff8340e8d141e3fb60ea24cfa587e964f07d")),
|
|
||||||
point(hex!("f4fa3898e7b08e039183d444f3d55040f3c790ed806cb314de49f3068bdbb218")),
|
|
||||||
point(hex!("0bbc37597c3ead517a3841e159c8b7b79a5ceaee24b2a9a20350127aab428713")),
|
|
||||||
],
|
|
||||||
R: vec![
|
|
||||||
point(hex!("609420ba1702781692e84accfd225adb3d077aedc3cf8125563400466b52dbd9")),
|
|
||||||
point(hex!("fb4e1d079e7a2b0ec14f7e2a3943bf50b6d60bc346a54fcf562fb234b342abf8")),
|
|
||||||
point(hex!("6ae3ac97289c48ce95b9c557289e82a34932055f7f5e32720139824fe81b12e5")),
|
|
||||||
point(hex!("d071cc2ffbdab2d840326ad15f68c01da6482271cae3cf644670d1632f29a15c")),
|
|
||||||
point(hex!("e52a1754b95e1060589ba7ce0c43d0060820ebfc0d49dc52884bc3c65ad18af5")),
|
|
||||||
point(hex!("41573b06140108539957df71aceb4b1816d2409ce896659aa5c86f037ca5e851")),
|
|
||||||
point(hex!("a65970b2cc3c7b08b2b5b739dbc8e71e646783c41c625e2a5b1535e3d2e0f742")),
|
|
||||||
],
|
|
||||||
a: scalar(hex!("0077c5383dea44d3cd1bc74849376bd60679612dc4b945255822457fa0c0a209")),
|
|
||||||
b: scalar(hex!("fe80cf5756473482581e1d38644007793ddc66fdeb9404ec1689a907e4863302")),
|
|
||||||
},
|
|
||||||
t_hat: scalar(hex!("40dfb08e09249040df997851db311bd6827c26e87d6f0f332c55be8eef10e603"))
|
|
||||||
})
|
|
||||||
.verify(
|
|
||||||
&mut OsRng,
|
|
||||||
&[
|
|
||||||
// For some reason, these vectors are * INV_EIGHT
|
|
||||||
point(hex!("8e8f23f315edae4f6c2f948d9a861e0ae32d356b933cd11d2f0e031ac744c41f"))
|
|
||||||
.mul_by_cofactor(),
|
|
||||||
point(hex!("2829cbd025aa54cd6e1b59a032564f22f0b2e5627f7f2c4297f90da438b5510f"))
|
|
||||||
.mul_by_cofactor(),
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
|
|
||||||
use curve25519_dalek::Scalar;
|
|
||||||
|
|
||||||
use monero_primitives::Commitment;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
batch_verifier::BulletproofsPlusBatchVerifier,
|
|
||||||
plus::aggregate_range_proof::{AggregateRangeStatement, AggregateRangeWitness},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_aggregate_range_proof() {
|
|
||||||
let mut verifier = BulletproofsPlusBatchVerifier::default();
|
|
||||||
for m in 1 ..= 16 {
|
|
||||||
let mut commitments = vec![];
|
|
||||||
for _ in 0 .. m {
|
|
||||||
commitments.push(Commitment::new(Scalar::random(&mut OsRng), OsRng.next_u64()));
|
|
||||||
}
|
|
||||||
let commitment_points = commitments.iter().map(Commitment::calculate).collect::<Vec<_>>();
|
|
||||||
let statement = AggregateRangeStatement::new(&commitment_points).unwrap();
|
|
||||||
let witness = AggregateRangeWitness::new(commitments).unwrap();
|
|
||||||
|
|
||||||
let proof = statement.clone().prove(&mut OsRng, &witness).unwrap();
|
|
||||||
statement.verify(&mut OsRng, &mut verifier, proof);
|
|
||||||
}
|
|
||||||
assert!(verifier.verify());
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#[cfg(test)]
|
|
||||||
mod weighted_inner_product;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod aggregate_range_proof;
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// The inner product relation is P = sum(g_bold * a, h_bold * b, g * (a * y * b), h * alpha)
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
batch_verifier::BulletproofsPlusBatchVerifier,
|
|
||||||
plus::{
|
|
||||||
ScalarVector, PointVector, GeneratorsList, BpPlusGenerators,
|
|
||||||
weighted_inner_product::{WipStatement, WipWitness},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_zero_weighted_inner_product() {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let P = EdwardsPoint::identity();
|
|
||||||
let y = Scalar::random(&mut OsRng);
|
|
||||||
|
|
||||||
let generators = BpPlusGenerators::new().reduce(1);
|
|
||||||
let statement = WipStatement::new(generators, P, y);
|
|
||||||
let witness = WipWitness::new(ScalarVector::new(1), ScalarVector::new(1), Scalar::ZERO).unwrap();
|
|
||||||
|
|
||||||
let transcript = Scalar::random(&mut OsRng);
|
|
||||||
let proof = statement.clone().prove(&mut OsRng, transcript, &witness).unwrap();
|
|
||||||
|
|
||||||
let mut verifier = BulletproofsPlusBatchVerifier::default();
|
|
||||||
statement.verify(&mut OsRng, &mut verifier, transcript, proof);
|
|
||||||
assert!(verifier.verify());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_weighted_inner_product() {
|
|
||||||
// P = sum(g_bold * a, h_bold * b, g * (a * y * b), h * alpha)
|
|
||||||
let mut verifier = BulletproofsPlusBatchVerifier::default();
|
|
||||||
let generators = BpPlusGenerators::new();
|
|
||||||
for i in [1, 2, 4, 8, 16, 32] {
|
|
||||||
let generators = generators.reduce(i);
|
|
||||||
let g = BpPlusGenerators::g();
|
|
||||||
let h = BpPlusGenerators::h();
|
|
||||||
assert_eq!(generators.len(), i);
|
|
||||||
let mut g_bold = vec![];
|
|
||||||
let mut h_bold = vec![];
|
|
||||||
for i in 0 .. i {
|
|
||||||
g_bold.push(generators.generator(GeneratorsList::GBold, i));
|
|
||||||
h_bold.push(generators.generator(GeneratorsList::HBold, i));
|
|
||||||
}
|
|
||||||
let g_bold = PointVector(g_bold);
|
|
||||||
let h_bold = PointVector(h_bold);
|
|
||||||
|
|
||||||
let mut a = ScalarVector::new(i);
|
|
||||||
let mut b = ScalarVector::new(i);
|
|
||||||
let alpha = Scalar::random(&mut OsRng);
|
|
||||||
|
|
||||||
let y = Scalar::random(&mut OsRng);
|
|
||||||
let mut y_vec = ScalarVector::new(g_bold.len());
|
|
||||||
y_vec[0] = y;
|
|
||||||
for i in 1 .. y_vec.len() {
|
|
||||||
y_vec[i] = y_vec[i - 1] * y;
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0 .. i {
|
|
||||||
a[i] = Scalar::random(&mut OsRng);
|
|
||||||
b[i] = Scalar::random(&mut OsRng);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let P = g_bold.multiexp(&a) +
|
|
||||||
h_bold.multiexp(&b) +
|
|
||||||
(g * a.clone().weighted_inner_product(&b, &y_vec)) +
|
|
||||||
(h * alpha);
|
|
||||||
|
|
||||||
let statement = WipStatement::new(generators, P, y);
|
|
||||||
let witness = WipWitness::new(a, b, alpha).unwrap();
|
|
||||||
|
|
||||||
let transcript = Scalar::random(&mut OsRng);
|
|
||||||
let proof = statement.clone().prove(&mut OsRng, transcript, &witness).unwrap();
|
|
||||||
statement.verify(&mut OsRng, &mut verifier, transcript, proof);
|
|
||||||
}
|
|
||||||
assert!(verifier.verify());
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-clsag"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "The CLSAG linkable ring signature, as defined by the Monero protocol"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/clsag"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
subtle = { version = "^2.4", default-features = false }
|
|
||||||
|
|
||||||
# Cryptographic dependencies
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
# Multisig dependencies
|
|
||||||
rand_chacha = { version = "0.3", default-features = false, optional = true }
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
|
||||||
group = { version = "0.13", default-features = false, optional = true }
|
|
||||||
dalek-ff-group = { path = "../../../../crypto/dalek-ff-group", version = "0.4", default-features = false, optional = true }
|
|
||||||
frost = { package = "modular-frost", path = "../../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true }
|
|
||||||
|
|
||||||
# Other Monero dependencies
|
|
||||||
monero-io = { path = "../../io", version = "0.1", default-features = false }
|
|
||||||
monero-generators = { path = "../../generators", version = "0.4", default-features = false }
|
|
||||||
monero-primitives = { path = "../../primitives", version = "0.1", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
frost = { package = "modular-frost", path = "../../../../crypto/frost", default-features = false, features = ["ed25519", "tests"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"rand_core/std",
|
|
||||||
"zeroize/std",
|
|
||||||
"subtle/std",
|
|
||||||
|
|
||||||
"rand_chacha?/std",
|
|
||||||
"transcript?/std",
|
|
||||||
"group?/alloc",
|
|
||||||
"dalek-ff-group?/std",
|
|
||||||
|
|
||||||
"monero-io/std",
|
|
||||||
"monero-generators/std",
|
|
||||||
"monero-primitives/std",
|
|
||||||
]
|
|
||||||
multisig = ["rand_chacha", "transcript", "group", "dalek-ff-group", "frost", "std"]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Monero CLSAG
|
|
||||||
|
|
||||||
The CLSAG linkable ring signature, as defined by the Monero protocol.
|
|
||||||
|
|
||||||
Additionally included is a FROST-inspired threshold multisignature algorithm.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
- `multisig`: Provides a FROST-inspired threshold multisignature algorithm for
|
|
||||||
use.
|
|
||||||
@@ -1,434 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use core::ops::Deref;
|
|
||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
|
||||||
use subtle::{ConstantTimeEq, ConditionallySelectable};
|
|
||||||
|
|
||||||
use curve25519_dalek::{
|
|
||||||
constants::{ED25519_BASEPOINT_TABLE, ED25519_BASEPOINT_POINT},
|
|
||||||
scalar::Scalar,
|
|
||||||
traits::{IsIdentity, MultiscalarMul, VartimePrecomputedMultiscalarMul},
|
|
||||||
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_io::*;
|
|
||||||
use monero_generators::hash_to_point;
|
|
||||||
use monero_primitives::{INV_EIGHT, G_PRECOMP, Commitment, Decoys, keccak256_to_scalar};
|
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
mod multisig;
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
pub use multisig::{ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig};
|
|
||||||
|
|
||||||
#[cfg(all(feature = "std", test))]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
/// Errors when working with CLSAGs.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum ClsagError {
|
|
||||||
/// The ring was invalid (such as being too small or too large).
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ring"))]
|
|
||||||
InvalidRing,
|
|
||||||
/// The discrete logarithm of the key, scaling G, wasn't equivalent to the signing ring member.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid commitment"))]
|
|
||||||
InvalidKey,
|
|
||||||
/// The commitment opening provided did not match the ring member's.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid commitment"))]
|
|
||||||
InvalidCommitment,
|
|
||||||
/// The key image was invalid (such as being identity or torsioned)
|
|
||||||
#[cfg_attr(feature = "std", error("invalid key image"))]
|
|
||||||
InvalidImage,
|
|
||||||
/// The `D` component was invalid.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid D"))]
|
|
||||||
InvalidD,
|
|
||||||
/// The `s` vector was invalid.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid s"))]
|
|
||||||
InvalidS,
|
|
||||||
/// The `c1` variable was invalid.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid c1"))]
|
|
||||||
InvalidC1,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Context on the input being signed for.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct ClsagContext {
|
|
||||||
// The opening for the commitment of the signing ring member
|
|
||||||
commitment: Commitment,
|
|
||||||
// Selected ring members' positions, signer index, and ring
|
|
||||||
decoys: Decoys,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClsagContext {
|
|
||||||
/// Create a new context, as necessary for signing.
|
|
||||||
pub fn new(decoys: Decoys, commitment: Commitment) -> Result<ClsagContext, ClsagError> {
|
|
||||||
if decoys.len() > u8::MAX.into() {
|
|
||||||
Err(ClsagError::InvalidRing)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the commitment matches
|
|
||||||
if decoys.signer_ring_members()[1] != commitment.calculate() {
|
|
||||||
Err(ClsagError::InvalidCommitment)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ClsagContext { commitment, decoys })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
enum Mode {
|
|
||||||
Sign { signer_index: u8, A: EdwardsPoint, AH: EdwardsPoint },
|
|
||||||
Verify { c1: Scalar, D_serialized: EdwardsPoint },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences
|
|
||||||
//
|
|
||||||
// Said differences are covered via the above Mode
|
|
||||||
fn core(
|
|
||||||
ring: &[[EdwardsPoint; 2]],
|
|
||||||
I: &EdwardsPoint,
|
|
||||||
pseudo_out: &EdwardsPoint,
|
|
||||||
msg_hash: &[u8; 32],
|
|
||||||
D_torsion_free: &EdwardsPoint,
|
|
||||||
s: &[Scalar],
|
|
||||||
A_c1: &Mode,
|
|
||||||
) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
|
|
||||||
let n = ring.len();
|
|
||||||
|
|
||||||
let images_precomp = match A_c1 {
|
|
||||||
Mode::Sign { .. } => None,
|
|
||||||
Mode::Verify { .. } => Some(VartimeEdwardsPrecomputation::new([I, D_torsion_free])),
|
|
||||||
};
|
|
||||||
let D_inv_eight = D_torsion_free * INV_EIGHT();
|
|
||||||
|
|
||||||
// Generate the transcript
|
|
||||||
// Instead of generating multiple, a single transcript is created and then edited as needed
|
|
||||||
const PREFIX: &[u8] = b"CLSAG_";
|
|
||||||
#[rustfmt::skip]
|
|
||||||
const AGG_0: &[u8] = b"agg_0";
|
|
||||||
#[rustfmt::skip]
|
|
||||||
const ROUND: &[u8] = b"round";
|
|
||||||
const PREFIX_AGG_0_LEN: usize = PREFIX.len() + AGG_0.len();
|
|
||||||
|
|
||||||
let mut to_hash = Vec::with_capacity(((2 * n) + 5) * 32);
|
|
||||||
to_hash.extend(PREFIX);
|
|
||||||
to_hash.extend(AGG_0);
|
|
||||||
to_hash.extend([0; 32 - PREFIX_AGG_0_LEN]);
|
|
||||||
|
|
||||||
let mut P = Vec::with_capacity(n);
|
|
||||||
for member in ring {
|
|
||||||
P.push(member[0]);
|
|
||||||
to_hash.extend(member[0].compress().to_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut C = Vec::with_capacity(n);
|
|
||||||
for member in ring {
|
|
||||||
C.push(member[1] - pseudo_out);
|
|
||||||
to_hash.extend(member[1].compress().to_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
to_hash.extend(I.compress().to_bytes());
|
|
||||||
match A_c1 {
|
|
||||||
Mode::Sign { .. } => {
|
|
||||||
to_hash.extend(D_inv_eight.compress().to_bytes());
|
|
||||||
}
|
|
||||||
Mode::Verify { D_serialized, .. } => {
|
|
||||||
to_hash.extend(D_serialized.compress().to_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
to_hash.extend(pseudo_out.compress().to_bytes());
|
|
||||||
// mu_P with agg_0
|
|
||||||
let mu_P = keccak256_to_scalar(&to_hash);
|
|
||||||
// mu_C with agg_1
|
|
||||||
to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
|
|
||||||
let mu_C = keccak256_to_scalar(&to_hash);
|
|
||||||
|
|
||||||
// Truncate it for the round transcript, altering the DST as needed
|
|
||||||
to_hash.truncate(((2 * n) + 1) * 32);
|
|
||||||
for i in 0 .. ROUND.len() {
|
|
||||||
to_hash[PREFIX.len() + i] = ROUND[i];
|
|
||||||
}
|
|
||||||
// Unfortunately, it's I D pseudo_out instead of pseudo_out I D, meaning this needs to be
|
|
||||||
// truncated just to add it back
|
|
||||||
to_hash.extend(pseudo_out.compress().to_bytes());
|
|
||||||
to_hash.extend(msg_hash);
|
|
||||||
|
|
||||||
// Configure the loop based on if we're signing or verifying
|
|
||||||
let start;
|
|
||||||
let end;
|
|
||||||
let mut c;
|
|
||||||
match A_c1 {
|
|
||||||
Mode::Sign { signer_index, A, AH } => {
|
|
||||||
let signer_index = usize::from(*signer_index);
|
|
||||||
start = signer_index + 1;
|
|
||||||
end = signer_index + n;
|
|
||||||
to_hash.extend(A.compress().to_bytes());
|
|
||||||
to_hash.extend(AH.compress().to_bytes());
|
|
||||||
c = keccak256_to_scalar(&to_hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
Mode::Verify { c1, .. } => {
|
|
||||||
start = 0;
|
|
||||||
end = n;
|
|
||||||
c = *c1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the core loop
|
|
||||||
let mut c1 = c;
|
|
||||||
for i in (start .. end).map(|i| i % n) {
|
|
||||||
let c_p = mu_P * c;
|
|
||||||
let c_c = mu_C * c;
|
|
||||||
|
|
||||||
// (s_i * G) + (c_p * P_i) + (c_c * C_i)
|
|
||||||
let L = match A_c1 {
|
|
||||||
Mode::Sign { .. } => {
|
|
||||||
EdwardsPoint::multiscalar_mul([s[i], c_p, c_c], [ED25519_BASEPOINT_POINT, P[i], C[i]])
|
|
||||||
}
|
|
||||||
Mode::Verify { .. } => {
|
|
||||||
G_PRECOMP().vartime_mixed_multiscalar_mul([s[i]], [c_p, c_c], [P[i], C[i]])
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let PH = hash_to_point(P[i].compress().0);
|
|
||||||
|
|
||||||
// (c_p * I) + (c_c * D) + (s_i * PH)
|
|
||||||
let R = match A_c1 {
|
|
||||||
Mode::Sign { .. } => {
|
|
||||||
EdwardsPoint::multiscalar_mul([c_p, c_c, s[i]], [I, D_torsion_free, &PH])
|
|
||||||
}
|
|
||||||
Mode::Verify { .. } => images_precomp
|
|
||||||
.as_ref()
|
|
||||||
.expect("value populated when verifying wasn't populated")
|
|
||||||
.vartime_mixed_multiscalar_mul([c_p, c_c], [s[i]], [PH]),
|
|
||||||
};
|
|
||||||
|
|
||||||
to_hash.truncate(((2 * n) + 3) * 32);
|
|
||||||
to_hash.extend(L.compress().to_bytes());
|
|
||||||
to_hash.extend(R.compress().to_bytes());
|
|
||||||
c = keccak256_to_scalar(&to_hash);
|
|
||||||
|
|
||||||
// This will only execute once and shouldn't need to be constant time. Making it constant time
|
|
||||||
// removes the risk of branch prediction creating timing differences depending on ring index
|
|
||||||
// however
|
|
||||||
c1.conditional_assign(&c, i.ct_eq(&(n - 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This first tuple is needed to continue signing, the latter is the c to be tested/worked with
|
|
||||||
((D_inv_eight, c * mu_P, c * mu_C), c1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The CLSAG signature, as used in Monero.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Clsag {
|
|
||||||
/// The difference of the commitment randomnesses, scaling the key image generator.
|
|
||||||
pub D: EdwardsPoint,
|
|
||||||
/// The responses for each ring member.
|
|
||||||
pub s: Vec<Scalar>,
|
|
||||||
/// The first challenge in the ring.
|
|
||||||
pub c1: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ClsagSignCore {
|
|
||||||
incomplete_clsag: Clsag,
|
|
||||||
pseudo_out: EdwardsPoint,
|
|
||||||
key_challenge: Scalar,
|
|
||||||
challenged_mask: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clsag {
|
|
||||||
// Sign core is the extension of core as needed for signing, yet is shared between single signer
|
|
||||||
// and multisig, hence why it's still core
|
|
||||||
fn sign_core<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
I: &EdwardsPoint,
|
|
||||||
input: &ClsagContext,
|
|
||||||
mask: Scalar,
|
|
||||||
msg_hash: &[u8; 32],
|
|
||||||
A: EdwardsPoint,
|
|
||||||
AH: EdwardsPoint,
|
|
||||||
) -> ClsagSignCore {
|
|
||||||
let signer_index = input.decoys.signer_index();
|
|
||||||
|
|
||||||
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
|
|
||||||
let mask_delta = input.commitment.mask - mask;
|
|
||||||
|
|
||||||
let H = hash_to_point(input.decoys.ring()[usize::from(signer_index)][0].compress().0);
|
|
||||||
let D = H * mask_delta;
|
|
||||||
let mut s = Vec::with_capacity(input.decoys.ring().len());
|
|
||||||
for _ in 0 .. input.decoys.ring().len() {
|
|
||||||
s.push(Scalar::random(rng));
|
|
||||||
}
|
|
||||||
let ((D, c_p, c_c), c1) = core(
|
|
||||||
input.decoys.ring(),
|
|
||||||
I,
|
|
||||||
&pseudo_out,
|
|
||||||
msg_hash,
|
|
||||||
&D,
|
|
||||||
&s,
|
|
||||||
&Mode::Sign { signer_index, A, AH },
|
|
||||||
);
|
|
||||||
|
|
||||||
ClsagSignCore {
|
|
||||||
incomplete_clsag: Clsag { D, s, c1 },
|
|
||||||
pseudo_out,
|
|
||||||
key_challenge: c_p,
|
|
||||||
challenged_mask: c_c * mask_delta,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sign CLSAG signatures for the provided inputs.
|
|
||||||
///
|
|
||||||
/// Monero ensures the rerandomized input commitments have the same value as the outputs by
|
|
||||||
/// checking `sum(rerandomized_input_commitments) - sum(output_commitments) == 0`. This requires
|
|
||||||
/// not only the amounts balance, yet also
|
|
||||||
/// `sum(input_commitment_masks) - sum(output_commitment_masks)`.
|
|
||||||
///
|
|
||||||
/// Monero solves this by following the wallet protocol to determine each output commitment's
|
|
||||||
/// randomness, then using random masks for all but the last input. The last input is
|
|
||||||
/// rerandomized to the necessary mask for the equation to balance.
|
|
||||||
///
|
|
||||||
/// Due to Monero having this behavior, it only makes sense to sign CLSAGs as a list, hence this
|
|
||||||
/// API being the way it is.
|
|
||||||
///
|
|
||||||
/// `inputs` is of the form (discrete logarithm of the key, context).
|
|
||||||
///
|
|
||||||
/// `sum_outputs` is for the sum of the output commitments' masks.
|
|
||||||
///
|
|
||||||
/// WARNING: This follows the Fiat-Shamir transcript format used by the Monero protocol, which
|
|
||||||
/// makes assumptions on what has already been transcripted and bound to within `msg_hash`. Do
|
|
||||||
/// not use this if you don't know what you're doing.
|
|
||||||
pub fn sign<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
mut inputs: Vec<(Zeroizing<Scalar>, ClsagContext)>,
|
|
||||||
sum_outputs: Scalar,
|
|
||||||
msg_hash: [u8; 32],
|
|
||||||
) -> Result<Vec<(Clsag, EdwardsPoint)>, ClsagError> {
|
|
||||||
// Create the key images
|
|
||||||
let mut key_image_generators = vec![];
|
|
||||||
let mut key_images = vec![];
|
|
||||||
for input in &inputs {
|
|
||||||
let key = input.1.decoys.signer_ring_members()[0];
|
|
||||||
|
|
||||||
// Check the key is consistent
|
|
||||||
if (ED25519_BASEPOINT_TABLE * input.0.deref()) != key {
|
|
||||||
Err(ClsagError::InvalidKey)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key_image_generator = hash_to_point(key.compress().0);
|
|
||||||
key_image_generators.push(key_image_generator);
|
|
||||||
key_images.push(key_image_generator * input.0.deref());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(inputs.len());
|
|
||||||
let mut sum_pseudo_outs = Scalar::ZERO;
|
|
||||||
for i in 0 .. inputs.len() {
|
|
||||||
let mask;
|
|
||||||
// If this is the last input, set the mask as described above
|
|
||||||
if i == (inputs.len() - 1) {
|
|
||||||
mask = sum_outputs - sum_pseudo_outs;
|
|
||||||
} else {
|
|
||||||
mask = Scalar::random(rng);
|
|
||||||
sum_pseudo_outs += mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut nonce = Zeroizing::new(Scalar::random(rng));
|
|
||||||
let ClsagSignCore { mut incomplete_clsag, pseudo_out, key_challenge, challenged_mask } =
|
|
||||||
Clsag::sign_core(
|
|
||||||
rng,
|
|
||||||
&key_images[i],
|
|
||||||
&inputs[i].1,
|
|
||||||
mask,
|
|
||||||
&msg_hash,
|
|
||||||
nonce.deref() * ED25519_BASEPOINT_TABLE,
|
|
||||||
nonce.deref() * key_image_generators[i],
|
|
||||||
);
|
|
||||||
// Effectively r - c x, except c x is (c_p x) + (c_c z), where z is the delta between the
|
|
||||||
// ring member's commitment and our pseudo-out commitment (which will only have a known
|
|
||||||
// discrete log over G if the amounts cancel out)
|
|
||||||
incomplete_clsag.s[usize::from(inputs[i].1.decoys.signer_index())] =
|
|
||||||
nonce.deref() - ((key_challenge * inputs[i].0.deref()) + challenged_mask);
|
|
||||||
let clsag = incomplete_clsag;
|
|
||||||
|
|
||||||
// Zeroize private keys and nonces.
|
|
||||||
inputs[i].0.zeroize();
|
|
||||||
nonce.zeroize();
|
|
||||||
|
|
||||||
debug_assert!(clsag
|
|
||||||
.verify(inputs[i].1.decoys.ring(), &key_images[i], &pseudo_out, &msg_hash)
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
res.push((clsag, pseudo_out));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify a CLSAG signature for the provided context.
|
|
||||||
///
|
|
||||||
/// WARNING: This follows the Fiat-Shamir transcript format used by the Monero protocol, which
|
|
||||||
/// makes assumptions on what has already been transcripted and bound to within `msg_hash`. Do
|
|
||||||
/// not use this if you don't know what you're doing.
|
|
||||||
pub fn verify(
|
|
||||||
&self,
|
|
||||||
ring: &[[EdwardsPoint; 2]],
|
|
||||||
I: &EdwardsPoint,
|
|
||||||
pseudo_out: &EdwardsPoint,
|
|
||||||
msg_hash: &[u8; 32],
|
|
||||||
) -> Result<(), ClsagError> {
|
|
||||||
// Preliminary checks
|
|
||||||
// s, c1, and points must also be encoded canonically, which is checked at time of decode
|
|
||||||
if ring.is_empty() {
|
|
||||||
Err(ClsagError::InvalidRing)?;
|
|
||||||
}
|
|
||||||
if ring.len() != self.s.len() {
|
|
||||||
Err(ClsagError::InvalidS)?;
|
|
||||||
}
|
|
||||||
if I.is_identity() || (!I.is_torsion_free()) {
|
|
||||||
Err(ClsagError::InvalidImage)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let D_torsion_free = self.D.mul_by_cofactor();
|
|
||||||
if D_torsion_free.is_identity() {
|
|
||||||
Err(ClsagError::InvalidD)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, c1) = core(
|
|
||||||
ring,
|
|
||||||
I,
|
|
||||||
pseudo_out,
|
|
||||||
msg_hash,
|
|
||||||
&D_torsion_free,
|
|
||||||
&self.s,
|
|
||||||
&Mode::Verify { c1: self.c1, D_serialized: self.D },
|
|
||||||
);
|
|
||||||
if c1 != self.c1 {
|
|
||||||
Err(ClsagError::InvalidC1)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a CLSAG.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
write_raw_vec(write_scalar, &self.s, w)?;
|
|
||||||
w.write_all(&self.c1.to_bytes())?;
|
|
||||||
write_point(&self.D, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a CLSAG.
|
|
||||||
pub fn read<R: Read>(decoys: usize, r: &mut R) -> io::Result<Clsag> {
|
|
||||||
Ok(Clsag { s: read_raw_vec(read_scalar, decoys, r)?, c1: read_scalar(r)?, D: read_point(r)? })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
use core::{ops::Deref, fmt::Debug};
|
|
||||||
use std_shims::{
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
io::{self, Read, Write},
|
|
||||||
collections::HashMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
|
||||||
use rand_chacha::ChaCha20Rng;
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use group::{
|
|
||||||
ff::{Field, PrimeField},
|
|
||||||
Group, GroupEncoding,
|
|
||||||
};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
use dalek_ff_group as dfg;
|
|
||||||
use frost::{
|
|
||||||
dkg::lagrange,
|
|
||||||
curve::Ed25519,
|
|
||||||
Participant, FrostError, ThresholdKeys, ThresholdView,
|
|
||||||
algorithm::{WriteAddendum, Algorithm},
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_generators::hash_to_point;
|
|
||||||
|
|
||||||
use crate::{ClsagContext, Clsag};
|
|
||||||
|
|
||||||
impl ClsagContext {
|
|
||||||
fn transcript<T: Transcript>(&self, transcript: &mut T) {
|
|
||||||
// Doesn't domain separate as this is considered part of the larger CLSAG proof
|
|
||||||
|
|
||||||
// Ring index
|
|
||||||
transcript.append_message(b"signer_index", [self.decoys.signer_index()]);
|
|
||||||
|
|
||||||
// Ring
|
|
||||||
for (i, pair) in self.decoys.ring().iter().enumerate() {
|
|
||||||
// Doesn't include global output indexes as CLSAG doesn't care/won't be affected by it
|
|
||||||
// They're just a unreliable reference to this data which will be included in the message
|
|
||||||
// if somehow relevant
|
|
||||||
transcript.append_message(b"member", [u8::try_from(i).expect("ring size exceeded 255")]);
|
|
||||||
// This also transcripts the key image generator since it's derived from this key
|
|
||||||
transcript.append_message(b"key", pair[0].compress().to_bytes());
|
|
||||||
transcript.append_message(b"commitment", pair[1].compress().to_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Doesn't include the commitment's parts as the above ring + index includes the commitment
|
|
||||||
// The only potential malleability would be if the G/H relationship is known, breaking the
|
|
||||||
// discrete log problem, which breaks everything already
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A channel to send the mask to use for the pseudo-out (rerandomized commitment) with.
|
|
||||||
///
|
|
||||||
/// A mask must be sent along this channel before any preprocess addendums are handled.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ClsagMultisigMaskSender {
|
|
||||||
buf: Arc<Mutex<Option<Scalar>>>,
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ClsagMultisigMaskReceiver {
|
|
||||||
buf: Arc<Mutex<Option<Scalar>>>,
|
|
||||||
}
|
|
||||||
impl ClsagMultisigMaskSender {
|
|
||||||
fn new() -> (ClsagMultisigMaskSender, ClsagMultisigMaskReceiver) {
|
|
||||||
let buf = Arc::new(Mutex::new(None));
|
|
||||||
(ClsagMultisigMaskSender { buf: buf.clone() }, ClsagMultisigMaskReceiver { buf })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a mask to a CLSAG multisig instance.
|
|
||||||
pub fn send(self, mask: Scalar) {
|
|
||||||
// There is no risk this was prior set as this consumes `self`, which does not implement
|
|
||||||
// `Clone`
|
|
||||||
*self.buf.lock() = Some(mask);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ClsagMultisigMaskReceiver {
|
|
||||||
fn recv(self) -> Option<Scalar> {
|
|
||||||
*self.buf.lock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Addendum produced during the signing process.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, Debug)]
|
|
||||||
pub struct ClsagAddendum {
|
|
||||||
key_image_share: dfg::EdwardsPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClsagAddendum {
|
|
||||||
/// The key image share within this addendum.
|
|
||||||
pub fn key_image_share(&self) -> dfg::EdwardsPoint {
|
|
||||||
self.key_image_share
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WriteAddendum for ClsagAddendum {
|
|
||||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(self.key_image_share.compress().to_bytes().as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
struct Interim {
|
|
||||||
p: Scalar,
|
|
||||||
c: Scalar,
|
|
||||||
|
|
||||||
clsag: Clsag,
|
|
||||||
pseudo_out: EdwardsPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FROST-inspired algorithm for producing a CLSAG signature.
|
|
||||||
///
|
|
||||||
/// Before this has its `process_addendum` called, a mask must be set.
|
|
||||||
///
|
|
||||||
/// The message signed is expected to be a 32-byte value. Per Monero, it's the keccak256 hash of
|
|
||||||
/// the transaction data which is signed. This will panic if the message is not a 32-byte value.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ClsagMultisig {
|
|
||||||
transcript: RecommendedTranscript,
|
|
||||||
|
|
||||||
key_image_generator: EdwardsPoint,
|
|
||||||
key_image_shares: HashMap<[u8; 32], dfg::EdwardsPoint>,
|
|
||||||
image: Option<dfg::EdwardsPoint>,
|
|
||||||
|
|
||||||
context: ClsagContext,
|
|
||||||
|
|
||||||
mask_recv: Option<ClsagMultisigMaskReceiver>,
|
|
||||||
mask: Option<Scalar>,
|
|
||||||
|
|
||||||
msg_hash: Option<[u8; 32]>,
|
|
||||||
interim: Option<Interim>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClsagMultisig {
|
|
||||||
/// Construct a new instance of multisignature CLSAG signing.
|
|
||||||
pub fn new(
|
|
||||||
transcript: RecommendedTranscript,
|
|
||||||
context: ClsagContext,
|
|
||||||
) -> (ClsagMultisig, ClsagMultisigMaskSender) {
|
|
||||||
let (mask_send, mask_recv) = ClsagMultisigMaskSender::new();
|
|
||||||
(
|
|
||||||
ClsagMultisig {
|
|
||||||
transcript,
|
|
||||||
|
|
||||||
key_image_generator: hash_to_point(context.decoys.signer_ring_members()[0].compress().0),
|
|
||||||
key_image_shares: HashMap::new(),
|
|
||||||
image: None,
|
|
||||||
|
|
||||||
context,
|
|
||||||
|
|
||||||
mask_recv: Some(mask_recv),
|
|
||||||
mask: None,
|
|
||||||
|
|
||||||
msg_hash: None,
|
|
||||||
interim: None,
|
|
||||||
},
|
|
||||||
mask_send,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The key image generator used by the signer.
|
|
||||||
pub fn key_image_generator(&self) -> EdwardsPoint {
|
|
||||||
self.key_image_generator
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Algorithm<Ed25519> for ClsagMultisig {
|
|
||||||
type Transcript = RecommendedTranscript;
|
|
||||||
type Addendum = ClsagAddendum;
|
|
||||||
// We output the CLSAG and the key image, which requires an interactive protocol to obtain
|
|
||||||
type Signature = (Clsag, EdwardsPoint);
|
|
||||||
|
|
||||||
// We need the nonce represented against both G and the key image generator
|
|
||||||
fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
|
|
||||||
vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.key_image_generator)]]
|
|
||||||
}
|
|
||||||
|
|
||||||
// We also publish our share of the key image
|
|
||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
_rng: &mut R,
|
|
||||||
keys: &ThresholdKeys<Ed25519>,
|
|
||||||
) -> ClsagAddendum {
|
|
||||||
ClsagAddendum {
|
|
||||||
key_image_share: dfg::EdwardsPoint(self.key_image_generator) * keys.secret_share().deref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<ClsagAddendum> {
|
|
||||||
let mut bytes = [0; 32];
|
|
||||||
reader.read_exact(&mut bytes)?;
|
|
||||||
// dfg ensures the point is torsion free
|
|
||||||
let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
|
|
||||||
.ok_or_else(|| io::Error::other("invalid key image"))?;
|
|
||||||
// Ensure this is a canonical point
|
|
||||||
if xH.to_bytes() != bytes {
|
|
||||||
Err(io::Error::other("non-canonical key image"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ClsagAddendum { key_image_share: xH })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_addendum(
|
|
||||||
&mut self,
|
|
||||||
view: &ThresholdView<Ed25519>,
|
|
||||||
l: Participant,
|
|
||||||
addendum: ClsagAddendum,
|
|
||||||
) -> Result<(), FrostError> {
|
|
||||||
if self.image.is_none() {
|
|
||||||
self.transcript.domain_separate(b"CLSAG");
|
|
||||||
// Transcript the ring
|
|
||||||
self.context.transcript(&mut self.transcript);
|
|
||||||
// Fetch the mask from the Mutex
|
|
||||||
// We set it to a variable to ensure our view of it is consistent
|
|
||||||
// It was this or a mpsc channel... std doesn't have oneshot :/
|
|
||||||
self.mask = Some(
|
|
||||||
self
|
|
||||||
.mask_recv
|
|
||||||
.take()
|
|
||||||
.expect("image was none multiple times, despite setting to Some on first iteration")
|
|
||||||
.recv()
|
|
||||||
.ok_or(FrostError::InternalError("CLSAG mask was not provided"))?,
|
|
||||||
);
|
|
||||||
// Transcript the mask
|
|
||||||
self.transcript.append_message(b"mask", self.mask.expect("mask wasn't set").to_bytes());
|
|
||||||
|
|
||||||
// Init the image to the offset
|
|
||||||
self.image = Some(dfg::EdwardsPoint(self.key_image_generator) * view.offset());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transcript this participant's contribution
|
|
||||||
self.transcript.append_message(b"participant", l.to_bytes());
|
|
||||||
self
|
|
||||||
.transcript
|
|
||||||
.append_message(b"key_image_share", addendum.key_image_share.compress().to_bytes());
|
|
||||||
|
|
||||||
// Accumulate the interpolated share
|
|
||||||
let interpolated_key_image_share =
|
|
||||||
addendum.key_image_share * lagrange::<dfg::Scalar>(l, view.included());
|
|
||||||
*self.image.as_mut().expect("image populated on first iteration wasn't Some") +=
|
|
||||||
interpolated_key_image_share;
|
|
||||||
|
|
||||||
self
|
|
||||||
.key_image_shares
|
|
||||||
.insert(view.verification_share(l).to_bytes(), interpolated_key_image_share);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
|
||||||
&mut self.transcript
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign_share(
|
|
||||||
&mut self,
|
|
||||||
view: &ThresholdView<Ed25519>,
|
|
||||||
nonce_sums: &[Vec<dfg::EdwardsPoint>],
|
|
||||||
nonces: Vec<Zeroizing<dfg::Scalar>>,
|
|
||||||
msg_hash: &[u8],
|
|
||||||
) -> dfg::Scalar {
|
|
||||||
// Use the transcript to get a seeded random number generator
|
|
||||||
//
|
|
||||||
// The transcript contains private data, preventing passive adversaries from recreating this
|
|
||||||
// process even if they have access to the commitments/key image share broadcast so far
|
|
||||||
//
|
|
||||||
// Specifically, the transcript contains the signer's index within the ring, along with the
|
|
||||||
// opening of the commitment being re-randomized (and what it's re-randomized to)
|
|
||||||
let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"decoy_responses"));
|
|
||||||
|
|
||||||
let msg_hash = msg_hash.try_into().expect("CLSAG message hash should be 32-bytes");
|
|
||||||
self.msg_hash = Some(msg_hash);
|
|
||||||
|
|
||||||
let sign_core = Clsag::sign_core(
|
|
||||||
&mut rng,
|
|
||||||
&self.image.expect("verifying a share despite never processing any addendums").0,
|
|
||||||
&self.context,
|
|
||||||
self.mask.expect("mask wasn't set"),
|
|
||||||
&msg_hash,
|
|
||||||
nonce_sums[0][0].0,
|
|
||||||
nonce_sums[0][1].0,
|
|
||||||
);
|
|
||||||
self.interim = Some(Interim {
|
|
||||||
p: sign_core.key_challenge,
|
|
||||||
c: sign_core.challenged_mask,
|
|
||||||
clsag: sign_core.incomplete_clsag,
|
|
||||||
pseudo_out: sign_core.pseudo_out,
|
|
||||||
});
|
|
||||||
|
|
||||||
// r - p x, where p is the challenge for the keys
|
|
||||||
*nonces[0] - dfg::Scalar(sign_core.key_challenge) * view.secret_share().deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn verify(
|
|
||||||
&self,
|
|
||||||
_: dfg::EdwardsPoint,
|
|
||||||
_: &[Vec<dfg::EdwardsPoint>],
|
|
||||||
sum: dfg::Scalar,
|
|
||||||
) -> Option<Self::Signature> {
|
|
||||||
let interim = self.interim.as_ref().expect("verify called before sign_share");
|
|
||||||
let mut clsag = interim.clsag.clone();
|
|
||||||
// We produced shares as `r - p x`, yet the signature is actually `r - p x - c x`
|
|
||||||
// Substract `c x` (saved as `c`) now
|
|
||||||
clsag.s[usize::from(self.context.decoys.signer_index())] = sum.0 - interim.c;
|
|
||||||
if clsag
|
|
||||||
.verify(
|
|
||||||
self.context.decoys.ring(),
|
|
||||||
&self.image.expect("verifying a signature despite never processing any addendums").0,
|
|
||||||
&interim.pseudo_out,
|
|
||||||
self.msg_hash.as_ref().expect("verify called before sign_share"),
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
return Some((clsag, interim.pseudo_out));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_share(
|
|
||||||
&self,
|
|
||||||
verification_share: dfg::EdwardsPoint,
|
|
||||||
nonces: &[Vec<dfg::EdwardsPoint>],
|
|
||||||
share: dfg::Scalar,
|
|
||||||
) -> Result<Vec<(dfg::Scalar, dfg::EdwardsPoint)>, ()> {
|
|
||||||
let interim = self.interim.as_ref().expect("verify_share called before sign_share");
|
|
||||||
|
|
||||||
// For a share `r - p x`, the following two equalities should hold:
|
|
||||||
// - `(r - p x)G == R.0 - pV`, where `V = xG`
|
|
||||||
// - `(r - p x)H == R.1 - pK`, where `K = xH` (the key image share)
|
|
||||||
//
|
|
||||||
// This is effectively a discrete log equality proof for:
|
|
||||||
// V, K over G, H
|
|
||||||
// with nonces
|
|
||||||
// R.0, R.1
|
|
||||||
// and solution
|
|
||||||
// s
|
|
||||||
//
|
|
||||||
// Which is a batch-verifiable rewrite of the traditional CP93 proof
|
|
||||||
// (and also writable as Generalized Schnorr Protocol)
|
|
||||||
//
|
|
||||||
// That means that given a proper challenge, this alone can be certainly argued to prove the
|
|
||||||
// key image share is well-formed and the provided signature so proves for that.
|
|
||||||
|
|
||||||
// This is a bit funky as it doesn't prove the nonces are well-formed however. They're part of
|
|
||||||
// the prover data/transcript for a CP93/GSP proof, not part of the statement. This practically
|
|
||||||
// is fine, for a variety of reasons (given a consistent `x`, a consistent `r` can be
|
|
||||||
// extracted, and the nonces as used in CLSAG are also part of its prover data/transcript).
|
|
||||||
|
|
||||||
let key_image_share = self.key_image_shares[&verification_share.to_bytes()];
|
|
||||||
|
|
||||||
// Hash every variable relevant here, using the hash output as the random weight
|
|
||||||
let mut weight_transcript =
|
|
||||||
RecommendedTranscript::new(b"monero-serai v0.1 ClsagMultisig::verify_share");
|
|
||||||
weight_transcript.append_message(b"G", dfg::EdwardsPoint::generator().to_bytes());
|
|
||||||
weight_transcript.append_message(b"H", self.key_image_generator.to_bytes());
|
|
||||||
weight_transcript.append_message(b"xG", verification_share.to_bytes());
|
|
||||||
weight_transcript.append_message(b"xH", key_image_share.to_bytes());
|
|
||||||
weight_transcript.append_message(b"rG", nonces[0][0].to_bytes());
|
|
||||||
weight_transcript.append_message(b"rH", nonces[0][1].to_bytes());
|
|
||||||
weight_transcript.append_message(b"c", dfg::Scalar(interim.p).to_repr());
|
|
||||||
weight_transcript.append_message(b"s", share.to_repr());
|
|
||||||
let weight = weight_transcript.challenge(b"weight");
|
|
||||||
let weight = dfg::Scalar(Scalar::from_bytes_mod_order_wide(&weight.into()));
|
|
||||||
|
|
||||||
let part_one = vec![
|
|
||||||
(share, dfg::EdwardsPoint::generator()),
|
|
||||||
// -(R.0 - pV) == -R.0 + pV
|
|
||||||
(-dfg::Scalar::ONE, nonces[0][0]),
|
|
||||||
(dfg::Scalar(interim.p), verification_share),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut part_two = vec![
|
|
||||||
(weight * share, dfg::EdwardsPoint(self.key_image_generator)),
|
|
||||||
// -(R.1 - pK) == -R.1 + pK
|
|
||||||
(-weight, nonces[0][1]),
|
|
||||||
(weight * dfg::Scalar(interim.p), key_image_share),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut all = part_one;
|
|
||||||
all.append(&mut part_two);
|
|
||||||
Ok(all)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
use frost::curve::Ed25519;
|
|
||||||
|
|
||||||
use monero_generators::hash_to_point;
|
|
||||||
use monero_primitives::{Commitment, Decoys};
|
|
||||||
use crate::{ClsagContext, Clsag};
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
use crate::ClsagMultisig;
|
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
use frost::{
|
|
||||||
Participant,
|
|
||||||
sign::AlgorithmMachine,
|
|
||||||
tests::{key_gen, algorithm_machines_without_clone, sign_without_clone},
|
|
||||||
};
|
|
||||||
|
|
||||||
const RING_LEN: u64 = 11;
|
|
||||||
const AMOUNT: u64 = 1337;
|
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
const RING_INDEX: u8 = 3;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn clsag() {
|
|
||||||
for real in 0 .. RING_LEN {
|
|
||||||
let msg_hash = [1; 32];
|
|
||||||
|
|
||||||
let mut secrets = (Zeroizing::new(Scalar::ZERO), Scalar::ZERO);
|
|
||||||
let mut ring = vec![];
|
|
||||||
for i in 0 .. RING_LEN {
|
|
||||||
let dest = Zeroizing::new(Scalar::random(&mut OsRng));
|
|
||||||
let mask = Scalar::random(&mut OsRng);
|
|
||||||
let amount;
|
|
||||||
if i == real {
|
|
||||||
secrets = (dest.clone(), mask);
|
|
||||||
amount = AMOUNT;
|
|
||||||
} else {
|
|
||||||
amount = OsRng.next_u64();
|
|
||||||
}
|
|
||||||
ring
|
|
||||||
.push([dest.deref() * ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (mut clsag, pseudo_out) = Clsag::sign(
|
|
||||||
&mut OsRng,
|
|
||||||
vec![(
|
|
||||||
secrets.0.clone(),
|
|
||||||
ClsagContext::new(
|
|
||||||
Decoys::new((1 ..= RING_LEN).collect(), u8::try_from(real).unwrap(), ring.clone())
|
|
||||||
.unwrap(),
|
|
||||||
Commitment::new(secrets.1, AMOUNT),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)],
|
|
||||||
Scalar::random(&mut OsRng),
|
|
||||||
msg_hash,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.swap_remove(0);
|
|
||||||
|
|
||||||
let image =
|
|
||||||
hash_to_point((ED25519_BASEPOINT_TABLE * secrets.0.deref()).compress().0) * secrets.0.deref();
|
|
||||||
clsag.verify(&ring, &image, &pseudo_out, &msg_hash).unwrap();
|
|
||||||
|
|
||||||
// make sure verification fails if we throw a random `c1` at it.
|
|
||||||
clsag.c1 = Scalar::random(&mut OsRng);
|
|
||||||
assert!(clsag.verify(&ring, &image, &pseudo_out, &msg_hash).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
#[test]
|
|
||||||
fn clsag_multisig() {
|
|
||||||
let keys = key_gen::<_, Ed25519>(&mut OsRng);
|
|
||||||
|
|
||||||
let randomness = Scalar::random(&mut OsRng);
|
|
||||||
let mut ring = vec![];
|
|
||||||
for i in 0 .. RING_LEN {
|
|
||||||
let dest;
|
|
||||||
let mask;
|
|
||||||
let amount;
|
|
||||||
if i != u64::from(RING_INDEX) {
|
|
||||||
dest = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
|
||||||
mask = Scalar::random(&mut OsRng);
|
|
||||||
amount = OsRng.next_u64();
|
|
||||||
} else {
|
|
||||||
dest = keys[&Participant::new(1).unwrap()].group_key().0;
|
|
||||||
mask = randomness;
|
|
||||||
amount = AMOUNT;
|
|
||||||
}
|
|
||||||
ring.push([dest, Commitment::new(mask, amount).calculate()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mask = Scalar::random(&mut OsRng);
|
|
||||||
let params = || {
|
|
||||||
let (algorithm, mask_send) = ClsagMultisig::new(
|
|
||||||
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
|
||||||
ClsagContext::new(
|
|
||||||
Decoys::new((1 ..= RING_LEN).collect(), RING_INDEX, ring.clone()).unwrap(),
|
|
||||||
Commitment::new(randomness, AMOUNT),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
mask_send.send(mask);
|
|
||||||
algorithm
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_without_clone(
|
|
||||||
&mut OsRng,
|
|
||||||
keys.clone(),
|
|
||||||
keys.values().map(|keys| (keys.params().i(), params())).collect(),
|
|
||||||
algorithm_machines_without_clone(
|
|
||||||
&mut OsRng,
|
|
||||||
&keys,
|
|
||||||
keys
|
|
||||||
.values()
|
|
||||||
.map(|keys| (keys.params().i(), AlgorithmMachine::new(params(), keys.clone())))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
&[1; 32],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-mlsag"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "The MLSAG linkable ring signature, as defined by the Monero protocol"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/mlsag"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
# Cryptographic dependencies
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
# Other Monero dependencies
|
|
||||||
monero-io = { path = "../../io", version = "0.1", default-features = false }
|
|
||||||
monero-generators = { path = "../../generators", version = "0.4", default-features = false }
|
|
||||||
monero-primitives = { path = "../../primitives", version = "0.1", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
|
|
||||||
"monero-io/std",
|
|
||||||
"monero-generators/std",
|
|
||||||
"monero-primitives/std",
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Monero MLSAG
|
|
||||||
|
|
||||||
The MLSAG linkable ring signature, as defined by the Monero protocol.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{traits::IsIdentity, Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_io::*;
|
|
||||||
use monero_generators::{H, hash_to_point};
|
|
||||||
use monero_primitives::keccak256_to_scalar;
|
|
||||||
|
|
||||||
/// Errors when working with MLSAGs.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum MlsagError {
|
|
||||||
/// Invalid ring (such as too small or too large).
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ring"))]
|
|
||||||
InvalidRing,
|
|
||||||
/// Invalid amount of key images.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid amount of key images"))]
|
|
||||||
InvalidAmountOfKeyImages,
|
|
||||||
/// Invalid ss matrix.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ss"))]
|
|
||||||
InvalidSs,
|
|
||||||
/// Invalid key image.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid key image"))]
|
|
||||||
InvalidKeyImage,
|
|
||||||
/// Invalid ci vector.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ci"))]
|
|
||||||
InvalidCi,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A vector of rings, forming a matrix, to verify the MLSAG with.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct RingMatrix {
|
|
||||||
matrix: Vec<Vec<EdwardsPoint>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RingMatrix {
|
|
||||||
/// Construct a ring matrix from an already formatted series of points.
|
|
||||||
fn new(matrix: Vec<Vec<EdwardsPoint>>) -> Result<Self, MlsagError> {
|
|
||||||
// Monero requires that there is more than one ring member for MLSAG signatures:
|
|
||||||
// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
|
|
||||||
// src/ringct/rctSigs.cpp#L462
|
|
||||||
if matrix.len() < 2 {
|
|
||||||
Err(MlsagError::InvalidRing)?;
|
|
||||||
}
|
|
||||||
for member in &matrix {
|
|
||||||
if member.is_empty() || (member.len() != matrix[0].len()) {
|
|
||||||
Err(MlsagError::InvalidRing)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RingMatrix { matrix })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct a ring matrix for an individual output.
|
|
||||||
pub fn individual(
|
|
||||||
ring: &[[EdwardsPoint; 2]],
|
|
||||||
pseudo_out: EdwardsPoint,
|
|
||||||
) -> Result<Self, MlsagError> {
|
|
||||||
let mut matrix = Vec::with_capacity(ring.len());
|
|
||||||
for ring_member in ring {
|
|
||||||
matrix.push(vec![ring_member[0], ring_member[1] - pseudo_out]);
|
|
||||||
}
|
|
||||||
RingMatrix::new(matrix)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterate over the members of the matrix.
|
|
||||||
fn iter(&self) -> impl Iterator<Item = &[EdwardsPoint]> {
|
|
||||||
self.matrix.iter().map(AsRef::as_ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the amount of members in the ring.
|
|
||||||
pub fn members(&self) -> usize {
|
|
||||||
self.matrix.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the length of a ring member.
|
|
||||||
///
|
|
||||||
/// A ring member is a vector of points for which the signer knows all of the discrete logarithms
|
|
||||||
/// of.
|
|
||||||
pub fn member_len(&self) -> usize {
|
|
||||||
// this is safe to do as the constructors don't allow empty rings
|
|
||||||
self.matrix[0].len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The MLSAG linkable ring signature, as used in Monero.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct Mlsag {
|
|
||||||
ss: Vec<Vec<Scalar>>,
|
|
||||||
cc: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mlsag {
|
|
||||||
/// Write a MLSAG.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for ss in &self.ss {
|
|
||||||
write_raw_vec(write_scalar, ss, w)?;
|
|
||||||
}
|
|
||||||
write_scalar(&self.cc, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a MLSAG.
|
|
||||||
pub fn read<R: Read>(mixins: usize, ss_2_elements: usize, r: &mut R) -> io::Result<Mlsag> {
|
|
||||||
Ok(Mlsag {
|
|
||||||
ss: (0 .. mixins)
|
|
||||||
.map(|_| read_raw_vec(read_scalar, ss_2_elements, r))
|
|
||||||
.collect::<Result<_, _>>()?,
|
|
||||||
cc: read_scalar(r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify a MLSAG.
|
|
||||||
///
|
|
||||||
/// WARNING: This follows the Fiat-Shamir transcript format used by the Monero protocol, which
|
|
||||||
/// makes assumptions on what has already been transcripted and bound to within `msg`. Do not use
|
|
||||||
/// this if you don't know what you're doing.
|
|
||||||
pub fn verify(
|
|
||||||
&self,
|
|
||||||
msg: &[u8; 32],
|
|
||||||
ring: &RingMatrix,
|
|
||||||
key_images: &[EdwardsPoint],
|
|
||||||
) -> Result<(), MlsagError> {
|
|
||||||
// Mlsag allows for layers to not need linkability, hence they don't need key images
|
|
||||||
// Monero requires that there is always only 1 non-linkable layer - the amount commitments.
|
|
||||||
if ring.member_len() != (key_images.len() + 1) {
|
|
||||||
Err(MlsagError::InvalidAmountOfKeyImages)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(6 * 32);
|
|
||||||
buf.extend_from_slice(msg);
|
|
||||||
|
|
||||||
let mut ci = self.cc;
|
|
||||||
|
|
||||||
// This is an iterator over the key images as options with an added entry of `None` at the
|
|
||||||
// end for the non-linkable layer
|
|
||||||
let key_images_iter = key_images.iter().map(|ki| Some(*ki)).chain(core::iter::once(None));
|
|
||||||
|
|
||||||
if ring.matrix.len() != self.ss.len() {
|
|
||||||
Err(MlsagError::InvalidSs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ring_member, ss) in ring.iter().zip(&self.ss) {
|
|
||||||
if ring_member.len() != ss.len() {
|
|
||||||
Err(MlsagError::InvalidSs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ((ring_member_entry, s), ki) in ring_member.iter().zip(ss).zip(key_images_iter.clone()) {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let L = EdwardsPoint::vartime_double_scalar_mul_basepoint(&ci, ring_member_entry, s);
|
|
||||||
|
|
||||||
let compressed_ring_member_entry = ring_member_entry.compress();
|
|
||||||
buf.extend_from_slice(compressed_ring_member_entry.as_bytes());
|
|
||||||
buf.extend_from_slice(L.compress().as_bytes());
|
|
||||||
|
|
||||||
// Not all dimensions need to be linkable, e.g. commitments, and only linkable layers need
|
|
||||||
// to have key images.
|
|
||||||
if let Some(ki) = ki {
|
|
||||||
if ki.is_identity() || (!ki.is_torsion_free()) {
|
|
||||||
Err(MlsagError::InvalidKeyImage)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let R = (s * hash_to_point(compressed_ring_member_entry.to_bytes())) + (ci * ki);
|
|
||||||
buf.extend_from_slice(R.compress().as_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ci = keccak256_to_scalar(&buf);
|
|
||||||
// keep the msg in the buffer.
|
|
||||||
buf.drain(msg.len() ..);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ci != self.cc {
|
|
||||||
Err(MlsagError::InvalidCi)?
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder for a RingMatrix when using an aggregate signature.
|
|
||||||
///
|
|
||||||
/// This handles the formatting as necessary.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct AggregateRingMatrixBuilder {
|
|
||||||
key_ring: Vec<Vec<EdwardsPoint>>,
|
|
||||||
amounts_ring: Vec<EdwardsPoint>,
|
|
||||||
sum_out: EdwardsPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AggregateRingMatrixBuilder {
|
|
||||||
/// Create a new AggregateRingMatrixBuilder.
|
|
||||||
///
|
|
||||||
/// This takes in the transaction's outputs' commitments and fee used.
|
|
||||||
pub fn new(commitments: &[EdwardsPoint], fee: u64) -> Self {
|
|
||||||
AggregateRingMatrixBuilder {
|
|
||||||
key_ring: vec![],
|
|
||||||
amounts_ring: vec![],
|
|
||||||
sum_out: commitments.iter().sum::<EdwardsPoint>() + (*H * Scalar::from(fee)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a ring of [output key, commitment] to the matrix.
|
|
||||||
pub fn push_ring(&mut self, ring: &[[EdwardsPoint; 2]]) -> Result<(), MlsagError> {
|
|
||||||
if self.key_ring.is_empty() {
|
|
||||||
self.key_ring = vec![vec![]; ring.len()];
|
|
||||||
// Now that we know the length of the ring, fill the `amounts_ring`.
|
|
||||||
self.amounts_ring = vec![-self.sum_out; ring.len()];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.amounts_ring.len() != ring.len()) || ring.is_empty() {
|
|
||||||
// All the rings in an aggregate matrix must be the same length.
|
|
||||||
return Err(MlsagError::InvalidRing);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, ring_member) in ring.iter().enumerate() {
|
|
||||||
self.key_ring[i].push(ring_member[0]);
|
|
||||||
self.amounts_ring[i] += ring_member[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build and return the [`RingMatrix`].
|
|
||||||
pub fn build(mut self) -> Result<RingMatrix, MlsagError> {
|
|
||||||
for (i, amount_commitment) in self.amounts_ring.drain(..).enumerate() {
|
|
||||||
self.key_ring[i].push(amount_commitment);
|
|
||||||
}
|
|
||||||
RingMatrix::new(self.key_ring)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-rpc"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Trait for an RPC connection to a Monero daemon, built around monero-serai"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
monero-serai = { path = "..", default-features = false }
|
|
||||||
monero-address = { path = "../wallet/address", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
"hex/std",
|
|
||||||
"serde/std",
|
|
||||||
"serde_json/std",
|
|
||||||
|
|
||||||
"monero-serai/std",
|
|
||||||
"monero-address/std",
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Monero RPC
|
|
||||||
|
|
||||||
Trait for an RPC connection to a Monero daemon, built around monero-serai.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-simple-request-rpc"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "RPC connection to a Monero daemon via simple-request, built around monero-serai"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["alloc", "std"] }
|
|
||||||
digest_auth = { version = "0.3", default-features = false }
|
|
||||||
simple-request = { path = "../../../../common/request", version = "0.1", default-features = false, features = ["tls"] }
|
|
||||||
tokio = { version = "1", default-features = false }
|
|
||||||
|
|
||||||
monero-rpc = { path = "..", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
monero-address = { path = "../../wallet/address", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", default-features = false, features = ["macros"] }
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Monero simple-request RPC
|
|
||||||
|
|
||||||
RPC connection to a Monero daemon via simple-request, built around monero-serai.
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
|
|
||||||
use core::future::Future;
|
|
||||||
use std::{sync::Arc, io::Read, time::Duration};
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use digest_auth::{WwwAuthenticateHeader, AuthContext};
|
|
||||||
use simple_request::{
|
|
||||||
hyper::{StatusCode, header::HeaderValue, Request},
|
|
||||||
Response, Client,
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_rpc::{RpcError, Rpc};
|
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
enum Authentication {
|
|
||||||
// If unauthenticated, use a single client
|
|
||||||
Unauthenticated(Client),
|
|
||||||
// If authenticated, use a single client which supports being locked and tracks its nonce
|
|
||||||
// This ensures that if a nonce is requested, another caller doesn't make a request invalidating
|
|
||||||
// it
|
|
||||||
Authenticated {
|
|
||||||
username: Zeroizing<String>,
|
|
||||||
password: Zeroizing<String>,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
connection: Arc<Mutex<(Option<(WwwAuthenticateHeader, u64)>, Client)>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An HTTP(S) transport for the RPC.
|
|
||||||
///
|
|
||||||
/// Requires tokio.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SimpleRequestRpc {
|
|
||||||
authentication: Authentication,
|
|
||||||
url: String,
|
|
||||||
request_timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimpleRequestRpc {
|
|
||||||
fn digest_auth_challenge(
|
|
||||||
response: &Response,
|
|
||||||
) -> Result<Option<(WwwAuthenticateHeader, u64)>, RpcError> {
|
|
||||||
Ok(if let Some(header) = response.headers().get("www-authenticate") {
|
|
||||||
Some((
|
|
||||||
digest_auth::parse(header.to_str().map_err(|_| {
|
|
||||||
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
|
||||||
})?)
|
|
||||||
.map_err(|_| RpcError::InvalidNode("invalid digest-auth response".to_string()))?,
|
|
||||||
0,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new HTTP(S) RPC connection.
|
|
||||||
///
|
|
||||||
/// A daemon requiring authentication can be used via including the username and password in the
|
|
||||||
/// URL.
|
|
||||||
pub async fn new(url: String) -> Result<SimpleRequestRpc, RpcError> {
|
|
||||||
Self::with_custom_timeout(url, DEFAULT_TIMEOUT).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new HTTP(S) RPC connection with a custom timeout.
|
|
||||||
///
|
|
||||||
/// A daemon requiring authentication can be used via including the username and password in the
|
|
||||||
/// URL.
|
|
||||||
pub async fn with_custom_timeout(
|
|
||||||
mut url: String,
|
|
||||||
request_timeout: Duration,
|
|
||||||
) -> Result<SimpleRequestRpc, RpcError> {
|
|
||||||
let authentication = if url.contains('@') {
|
|
||||||
// Parse out the username and password
|
|
||||||
let url_clone = Zeroizing::new(url);
|
|
||||||
let split_url = url_clone.split('@').collect::<Vec<_>>();
|
|
||||||
if split_url.len() != 2 {
|
|
||||||
Err(RpcError::ConnectionError("invalid amount of login specifications".to_string()))?;
|
|
||||||
}
|
|
||||||
let mut userpass = split_url[0];
|
|
||||||
url = split_url[1].to_string();
|
|
||||||
|
|
||||||
// If there was additionally a protocol string, restore that to the daemon URL
|
|
||||||
if userpass.contains("://") {
|
|
||||||
let split_userpass = userpass.split("://").collect::<Vec<_>>();
|
|
||||||
if split_userpass.len() != 2 {
|
|
||||||
Err(RpcError::ConnectionError("invalid amount of protocol specifications".to_string()))?;
|
|
||||||
}
|
|
||||||
url = split_userpass[0].to_string() + "://" + &url;
|
|
||||||
userpass = split_userpass[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
let split_userpass = userpass.split(':').collect::<Vec<_>>();
|
|
||||||
if split_userpass.len() > 2 {
|
|
||||||
Err(RpcError::ConnectionError("invalid amount of passwords".to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = Client::without_connection_pool(&url)
|
|
||||||
.map_err(|_| RpcError::ConnectionError("invalid URL".to_string()))?;
|
|
||||||
// Obtain the initial challenge, which also somewhat validates this connection
|
|
||||||
let challenge = Self::digest_auth_challenge(
|
|
||||||
&client
|
|
||||||
.request(
|
|
||||||
Request::post(url.clone())
|
|
||||||
.body(vec![].into())
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))?,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
||||||
)?;
|
|
||||||
Authentication::Authenticated {
|
|
||||||
username: Zeroizing::new(split_userpass[0].to_string()),
|
|
||||||
password: Zeroizing::new((*split_userpass.get(1).unwrap_or(&"")).to_string()),
|
|
||||||
connection: Arc::new(Mutex::new((challenge, client))),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Authentication::Unauthenticated(Client::with_connection_pool())
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(SimpleRequestRpc { authentication, url, request_timeout })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimpleRequestRpc {
|
|
||||||
async fn inner_post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
|
||||||
let request_fn = |uri| {
|
|
||||||
Request::post(uri)
|
|
||||||
.body(body.clone().into())
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn body_from_response(response: Response<'_>) -> Result<Vec<u8>, RpcError> {
|
|
||||||
let mut res = Vec::with_capacity(128);
|
|
||||||
response
|
|
||||||
.body()
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
|
||||||
.read_to_end(&mut res)
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
for attempt in 0 .. 2 {
|
|
||||||
return Ok(match &self.authentication {
|
|
||||||
Authentication::Unauthenticated(client) => {
|
|
||||||
body_from_response(
|
|
||||||
client
|
|
||||||
.request(request_fn(self.url.clone() + "/" + route)?)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
Authentication::Authenticated { username, password, connection } => {
|
|
||||||
let mut connection_lock = connection.lock().await;
|
|
||||||
|
|
||||||
let mut request = request_fn("/".to_string() + route)?;
|
|
||||||
|
|
||||||
// If we don't have an auth challenge, obtain one
|
|
||||||
if connection_lock.0.is_none() {
|
|
||||||
connection_lock.0 = Self::digest_auth_challenge(
|
|
||||||
&connection_lock
|
|
||||||
.1
|
|
||||||
.request(request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
||||||
)?;
|
|
||||||
request = request_fn("/".to_string() + route)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert the challenge response, if we have a challenge
|
|
||||||
if let Some((challenge, cnonce)) = connection_lock.0.as_mut() {
|
|
||||||
// Update the cnonce
|
|
||||||
// Overflow isn't a concern as this is a u64
|
|
||||||
*cnonce += 1;
|
|
||||||
|
|
||||||
let mut context = AuthContext::new_post::<_, _, _, &[u8]>(
|
|
||||||
<_ as AsRef<str>>::as_ref(username),
|
|
||||||
<_ as AsRef<str>>::as_ref(password),
|
|
||||||
"/".to_string() + route,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
context.set_custom_cnonce(hex::encode(cnonce.to_le_bytes()));
|
|
||||||
|
|
||||||
request.headers_mut().insert(
|
|
||||||
"Authorization",
|
|
||||||
HeaderValue::from_str(
|
|
||||||
&challenge
|
|
||||||
.respond(&context)
|
|
||||||
.map_err(|_| {
|
|
||||||
RpcError::InvalidNode("couldn't respond to digest-auth challenge".to_string())
|
|
||||||
})?
|
|
||||||
.to_header_string(),
|
|
||||||
)
|
|
||||||
.map_err(|_| {
|
|
||||||
RpcError::InternalError(
|
|
||||||
"digest-auth challenge response wasn't a valid string for an HTTP header"
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = connection_lock
|
|
||||||
.1
|
|
||||||
.request(request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")));
|
|
||||||
|
|
||||||
let (error, is_stale) = match &response {
|
|
||||||
Err(e) => (Some(e.clone()), false),
|
|
||||||
Ok(response) => (
|
|
||||||
None,
|
|
||||||
if response.status() == StatusCode::UNAUTHORIZED {
|
|
||||||
if let Some(header) = response.headers().get("www-authenticate") {
|
|
||||||
header
|
|
||||||
.to_str()
|
|
||||||
.map_err(|_| {
|
|
||||||
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
|
||||||
})?
|
|
||||||
.contains("stale")
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the connection entered an error state, drop the cached challenge as challenges are
|
|
||||||
// per-connection
|
|
||||||
// We don't need to create a new connection as simple-request will for us
|
|
||||||
if error.is_some() || is_stale {
|
|
||||||
connection_lock.0 = None;
|
|
||||||
// If we're not already on our second attempt, move to the next loop iteration
|
|
||||||
// (retrying all of this once)
|
|
||||||
if attempt == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(e) = error {
|
|
||||||
Err(e)?
|
|
||||||
} else {
|
|
||||||
debug_assert!(is_stale);
|
|
||||||
Err(RpcError::InvalidNode(
|
|
||||||
"node claimed fresh connection had stale authentication".to_string(),
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body_from_response(response.expect("no response yet also no error?")).await?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rpc for SimpleRequestRpc {
|
|
||||||
fn post(
|
|
||||||
&self,
|
|
||||||
route: &str,
|
|
||||||
body: Vec<u8>,
|
|
||||||
) -> impl Send + Future<Output = Result<Vec<u8>, RpcError>> {
|
|
||||||
async move {
|
|
||||||
tokio::time::timeout(self.request_timeout, self.inner_post(route, body))
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
use std::sync::LazyLock;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use monero_address::{Network, MoneroAddress};
|
|
||||||
|
|
||||||
// monero-rpc doesn't include a transport
|
|
||||||
// We can't include the simple-request crate there as then we'd have a cyclical dependency
|
|
||||||
// Accordingly, we test monero-rpc here (implicitly testing the simple-request transport)
|
|
||||||
use monero_simple_request_rpc::*;
|
|
||||||
|
|
||||||
static SEQUENTIAL: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
|
||||||
|
|
||||||
const ADDRESS: &str =
|
|
||||||
"4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_rpc() {
|
|
||||||
use monero_rpc::Rpc;
|
|
||||||
|
|
||||||
let guard = SEQUENTIAL.lock().await;
|
|
||||||
|
|
||||||
let rpc =
|
|
||||||
SimpleRequestRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap();
|
|
||||||
|
|
||||||
{
|
|
||||||
// Test get_height
|
|
||||||
let height = rpc.get_height().await.unwrap();
|
|
||||||
// The height should be the amount of blocks on chain
|
|
||||||
// The number of a block should be its zero-indexed position
|
|
||||||
// Accordingly, there should be no block whose number is the height
|
|
||||||
assert!(rpc.get_block_by_number(height).await.is_err());
|
|
||||||
let block_number = height - 1;
|
|
||||||
// There should be a block just prior
|
|
||||||
let block = rpc.get_block_by_number(block_number).await.unwrap();
|
|
||||||
|
|
||||||
// Also test the block RPC routes are consistent
|
|
||||||
assert_eq!(block.number().unwrap(), block_number);
|
|
||||||
assert_eq!(rpc.get_block(block.hash()).await.unwrap(), block);
|
|
||||||
assert_eq!(rpc.get_block_hash(block_number).await.unwrap(), block.hash());
|
|
||||||
|
|
||||||
// And finally the hardfork version route
|
|
||||||
assert_eq!(rpc.get_hardfork_version().await.unwrap(), block.header.hardfork_version);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test generate_blocks
|
|
||||||
for amount_of_blocks in [1, 5] {
|
|
||||||
let (blocks, number) = rpc
|
|
||||||
.generate_blocks(
|
|
||||||
&MoneroAddress::from_str(Network::Mainnet, ADDRESS).unwrap(),
|
|
||||||
amount_of_blocks,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let height = rpc.get_height().await.unwrap();
|
|
||||||
assert_eq!(number, height - 1);
|
|
||||||
|
|
||||||
let mut actual_blocks = Vec::with_capacity(amount_of_blocks);
|
|
||||||
for i in (height - amount_of_blocks) .. height {
|
|
||||||
actual_blocks.push(rpc.get_block_by_number(i).await.unwrap().hash());
|
|
||||||
}
|
|
||||||
assert_eq!(blocks, actual_blocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_decoy_rpc() {
|
|
||||||
use monero_rpc::{Rpc, DecoyRpc};
|
|
||||||
|
|
||||||
let guard = SEQUENTIAL.lock().await;
|
|
||||||
|
|
||||||
let rpc =
|
|
||||||
SimpleRequestRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap();
|
|
||||||
|
|
||||||
// Ensure there's blocks on-chain
|
|
||||||
rpc
|
|
||||||
.generate_blocks(&MoneroAddress::from_str(Network::Mainnet, ADDRESS).unwrap(), 100)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Test get_output_distribution
|
|
||||||
// Our documentation for our Rust fn defines it as taking two block numbers
|
|
||||||
{
|
|
||||||
let distribution_len = rpc.get_output_distribution_end_height().await.unwrap();
|
|
||||||
assert_eq!(distribution_len, rpc.get_height().await.unwrap());
|
|
||||||
|
|
||||||
rpc.get_output_distribution(0 ..= distribution_len).await.unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
rpc.get_output_distribution(0 .. distribution_len).await.unwrap().len(),
|
|
||||||
distribution_len
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
rpc.get_output_distribution(.. distribution_len).await.unwrap().len(),
|
|
||||||
distribution_len
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rpc.get_output_distribution(.. (distribution_len - 1)).await.unwrap().len(),
|
|
||||||
distribution_len - 1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
rpc.get_output_distribution(1 .. distribution_len).await.unwrap().len(),
|
|
||||||
distribution_len - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(rpc.get_output_distribution(0 ..= 0).await.unwrap().len(), 1);
|
|
||||||
assert_eq!(rpc.get_output_distribution(0 ..= 1).await.unwrap().len(), 2);
|
|
||||||
assert_eq!(rpc.get_output_distribution(1 ..= 1).await.unwrap().len(), 1);
|
|
||||||
|
|
||||||
rpc.get_output_distribution(0 .. 0).await.unwrap_err();
|
|
||||||
#[allow(clippy::reversed_empty_ranges)]
|
|
||||||
rpc.get_output_distribution(1 .. 0).await.unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test passes yet requires a mainnet node, which we don't have reliable access to in CI.
|
|
||||||
/*
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_zero_out_tx_o_indexes() {
|
|
||||||
use monero_rpc::Rpc;
|
|
||||||
|
|
||||||
let guard = SEQUENTIAL.lock().await;
|
|
||||||
|
|
||||||
let rpc = SimpleRequestRpc::new("https://node.sethforprivacy.com".to_string()).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rpc
|
|
||||||
.get_o_indexes(
|
|
||||||
hex::decode("17ce4c8feeb82a6d6adaa8a89724b32bf4456f6909c7f84c8ce3ee9ebba19163")
|
|
||||||
.unwrap()
|
|
||||||
.try_into()
|
|
||||||
.unwrap()
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
Vec::<u64>::new()
|
|
||||||
);
|
|
||||||
|
|
||||||
drop(guard);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
io::*,
|
|
||||||
primitives::keccak256,
|
|
||||||
merkle::merkle_root,
|
|
||||||
transaction::{Input, Transaction},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CORRECT_BLOCK_HASH_202612: [u8; 32] =
|
|
||||||
hex_literal::hex!("426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a");
|
|
||||||
const EXISTING_BLOCK_HASH_202612: [u8; 32] =
|
|
||||||
hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698");
|
|
||||||
|
|
||||||
/// A Monero block's header.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct BlockHeader {
|
|
||||||
/// The hard fork of the protocol this block follows.
|
|
||||||
///
|
|
||||||
/// Per the C++ codebase, this is the `major_version`.
|
|
||||||
pub hardfork_version: u8,
|
|
||||||
/// A signal for a proposed hard fork.
|
|
||||||
///
|
|
||||||
/// Per the C++ codebase, this is the `minor_version`.
|
|
||||||
pub hardfork_signal: u8,
|
|
||||||
/// Seconds since the epoch.
|
|
||||||
pub timestamp: u64,
|
|
||||||
/// The previous block's hash.
|
|
||||||
pub previous: [u8; 32],
|
|
||||||
/// The nonce used to mine the block.
|
|
||||||
///
|
|
||||||
/// Miners should increment this while attempting to find a block with a hash satisfying the PoW
|
|
||||||
/// rules.
|
|
||||||
pub nonce: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlockHeader {
|
|
||||||
/// Write the BlockHeader.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
write_varint(&self.hardfork_version, w)?;
|
|
||||||
write_varint(&self.hardfork_signal, w)?;
|
|
||||||
write_varint(&self.timestamp, w)?;
|
|
||||||
w.write_all(&self.previous)?;
|
|
||||||
w.write_all(&self.nonce.to_le_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the BlockHeader to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = vec![];
|
|
||||||
self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a BlockHeader.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<BlockHeader> {
|
|
||||||
Ok(BlockHeader {
|
|
||||||
hardfork_version: read_varint(r)?,
|
|
||||||
hardfork_signal: read_varint(r)?,
|
|
||||||
timestamp: read_varint(r)?,
|
|
||||||
previous: read_bytes(r)?,
|
|
||||||
nonce: read_bytes(r).map(u32::from_le_bytes)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Monero block.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Block {
|
|
||||||
/// The block's header.
|
|
||||||
pub header: BlockHeader,
|
|
||||||
/// The miner's transaction.
|
|
||||||
pub miner_transaction: Transaction,
|
|
||||||
/// The transactions within this block.
|
|
||||||
pub transactions: Vec<[u8; 32]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Block {
|
|
||||||
/// The zero-indexed position of this block within the blockchain.
|
|
||||||
///
|
|
||||||
/// This information comes from the Block's miner transaction. If the miner transaction isn't
|
|
||||||
/// structed as expected, this will return None. This will return Some for any Block which would
|
|
||||||
/// pass the consensus rules.
|
|
||||||
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
|
||||||
// /src/cryptonote_core/blockchain.cpp#L1365-L1382
|
|
||||||
pub fn number(&self) -> Option<usize> {
|
|
||||||
match &self.miner_transaction {
|
|
||||||
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => {
|
|
||||||
match prefix.inputs.first() {
|
|
||||||
Some(Input::Gen(number)) => Some(*number),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Block.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.header.write(w)?;
|
|
||||||
self.miner_transaction.write(w)?;
|
|
||||||
write_varint(&self.transactions.len(), w)?;
|
|
||||||
for tx in &self.transactions {
|
|
||||||
w.write_all(tx)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the Block to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = vec![];
|
|
||||||
self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the block as required for the proof of work hash.
|
|
||||||
///
|
|
||||||
/// This is distinct from the serialization required for the block hash. To get the block hash,
|
|
||||||
/// use the [`Block::hash`] function.
|
|
||||||
pub fn serialize_pow_hash(&self) -> Vec<u8> {
|
|
||||||
let mut blob = self.header.serialize();
|
|
||||||
blob.extend_from_slice(&merkle_root(self.miner_transaction.hash(), &self.transactions));
|
|
||||||
write_varint(
|
|
||||||
&(1 +
|
|
||||||
u64::try_from(self.transactions.len())
|
|
||||||
.expect("amount of transactions in block exceeded u64::MAX")),
|
|
||||||
&mut blob,
|
|
||||||
)
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
blob
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the hash of this block.
|
|
||||||
pub fn hash(&self) -> [u8; 32] {
|
|
||||||
let mut hashable = self.serialize_pow_hash();
|
|
||||||
// Monero pre-appends a VarInt of the block-to-hash'ss length before getting the block hash,
|
|
||||||
// but doesn't do this when getting the proof of work hash :)
|
|
||||||
let mut hashing_blob = Vec::with_capacity(9 + hashable.len());
|
|
||||||
write_varint(
|
|
||||||
&u64::try_from(hashable.len()).expect("length of block hash's preimage exceeded u64::MAX"),
|
|
||||||
&mut hashing_blob,
|
|
||||||
)
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
hashing_blob.append(&mut hashable);
|
|
||||||
|
|
||||||
let hash = keccak256(hashing_blob);
|
|
||||||
if hash == CORRECT_BLOCK_HASH_202612 {
|
|
||||||
return EXISTING_BLOCK_HASH_202612;
|
|
||||||
};
|
|
||||||
hash
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Block.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Block> {
|
|
||||||
Ok(Block {
|
|
||||||
header: BlockHeader::read(r)?,
|
|
||||||
miner_transaction: Transaction::read(r)?,
|
|
||||||
transactions: (0_usize .. read_varint(r)?)
|
|
||||||
.map(|_| read_bytes(r))
|
|
||||||
.collect::<Result<_, _>>()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
pub use monero_io as io;
|
|
||||||
pub use monero_generators as generators;
|
|
||||||
pub use monero_primitives as primitives;
|
|
||||||
|
|
||||||
mod merkle;
|
|
||||||
|
|
||||||
/// Ring Signature structs and functionality.
|
|
||||||
pub mod ring_signatures;
|
|
||||||
|
|
||||||
/// RingCT structs and functionality.
|
|
||||||
pub mod ringct;
|
|
||||||
|
|
||||||
/// Transaction structs and functionality.
|
|
||||||
pub mod transaction;
|
|
||||||
/// Block structs and functionality.
|
|
||||||
pub mod block;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
/// The minimum amount of blocks an output is locked for.
|
|
||||||
///
|
|
||||||
/// If Monero suffered a re-organization, any transactions which selected decoys belonging to
|
|
||||||
/// recent blocks would become invalidated. Accordingly, transactions must use decoys which are
|
|
||||||
/// presumed to not be invalidated in the future. If wallets only selected n-block-old outputs as
|
|
||||||
/// decoys, then any ring member within the past n blocks would have to be the real spend.
|
|
||||||
/// Preventing this at the consensus layer ensures privacy and integrity.
|
|
||||||
pub const DEFAULT_LOCK_WINDOW: usize = 10;
|
|
||||||
|
|
||||||
/// The minimum amount of blocks a coinbase output is locked for.
|
|
||||||
pub const COINBASE_LOCK_WINDOW: usize = 60;
|
|
||||||
|
|
||||||
/// Monero's block time target, in seconds.
|
|
||||||
pub const BLOCK_TIME: usize = 120;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use crate::primitives::keccak256;
|
|
||||||
|
|
||||||
pub(crate) fn merkle_root(root: [u8; 32], leafs: &[[u8; 32]]) -> [u8; 32] {
|
|
||||||
match leafs.len() {
|
|
||||||
0 => root,
|
|
||||||
1 => keccak256([root, leafs[0]].concat()),
|
|
||||||
_ => {
|
|
||||||
let mut hashes = Vec::with_capacity(1 + leafs.len());
|
|
||||||
hashes.push(root);
|
|
||||||
hashes.extend(leafs);
|
|
||||||
|
|
||||||
// Monero preprocess this so the length is a power of 2
|
|
||||||
let mut high_pow_2 = 4; // 4 is the lowest value this can be
|
|
||||||
while high_pow_2 < hashes.len() {
|
|
||||||
high_pow_2 *= 2;
|
|
||||||
}
|
|
||||||
let low_pow_2 = high_pow_2 / 2;
|
|
||||||
|
|
||||||
// Merge right-most hashes until we're at the low_pow_2
|
|
||||||
{
|
|
||||||
let overage = hashes.len() - low_pow_2;
|
|
||||||
let mut rightmost = hashes.drain((low_pow_2 - overage) ..);
|
|
||||||
// This is true since we took overage from beneath and above low_pow_2, taking twice as
|
|
||||||
// many elements as overage
|
|
||||||
debug_assert_eq!(rightmost.len() % 2, 0);
|
|
||||||
|
|
||||||
let mut paired_hashes = Vec::with_capacity(overage);
|
|
||||||
while let Some(left) = rightmost.next() {
|
|
||||||
let right = rightmost.next().expect("rightmost is of even length");
|
|
||||||
paired_hashes.push(keccak256([left.as_ref(), &right].concat()));
|
|
||||||
}
|
|
||||||
drop(rightmost);
|
|
||||||
|
|
||||||
hashes.extend(paired_hashes);
|
|
||||||
assert_eq!(hashes.len(), low_pow_2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a traditional pairing off
|
|
||||||
let mut new_hashes = Vec::with_capacity(hashes.len() / 2);
|
|
||||||
while hashes.len() > 1 {
|
|
||||||
let mut i = 0;
|
|
||||||
while i < hashes.len() {
|
|
||||||
new_hashes.push(keccak256([hashes[i], hashes[i + 1]].concat()));
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
hashes = new_hashes;
|
|
||||||
new_hashes = Vec::with_capacity(hashes.len() / 2);
|
|
||||||
}
|
|
||||||
hashes[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
io::{self, *},
|
|
||||||
vec::Vec,
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{EdwardsPoint, Scalar};
|
|
||||||
|
|
||||||
use crate::{io::*, generators::hash_to_point, primitives::keccak256_to_scalar};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub(crate) struct Signature {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) c: Scalar,
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) s: Scalar,
|
|
||||||
#[cfg(not(test))]
|
|
||||||
c: Scalar,
|
|
||||||
#[cfg(not(test))]
|
|
||||||
s: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signature {
|
|
||||||
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
write_scalar(&self.c, w)?;
|
|
||||||
write_scalar(&self.s, w)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
|
|
||||||
Ok(Signature { c: read_scalar(r)?, s: read_scalar(r)? })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A ring signature.
|
|
||||||
///
|
|
||||||
/// This was used by the original Cryptonote transaction protocol and was deprecated with RingCT.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct RingSignature {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) sigs: Vec<Signature>,
|
|
||||||
#[cfg(not(test))]
|
|
||||||
sigs: Vec<Signature>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RingSignature {
|
|
||||||
/// Write the RingSignature.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for sig in &self.sigs {
|
|
||||||
sig.write(w)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a RingSignature.
|
|
||||||
pub fn read<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> {
|
|
||||||
Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify the ring signature.
|
|
||||||
pub fn verify(&self, msg: &[u8; 32], ring: &[EdwardsPoint], key_image: &EdwardsPoint) -> bool {
|
|
||||||
if ring.len() != self.sigs.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(32 + (2 * 32 * ring.len()));
|
|
||||||
buf.extend_from_slice(msg);
|
|
||||||
|
|
||||||
let mut sum = Scalar::ZERO;
|
|
||||||
for (ring_member, sig) in ring.iter().zip(&self.sigs) {
|
|
||||||
/*
|
|
||||||
The traditional Schnorr signature is:
|
|
||||||
r = sample()
|
|
||||||
c = H(r G || m)
|
|
||||||
s = r - c x
|
|
||||||
Verified as:
|
|
||||||
s G + c A == R
|
|
||||||
|
|
||||||
Each ring member here performs a dual-Schnorr signature for:
|
|
||||||
s G + c A
|
|
||||||
s HtP(A) + c K
|
|
||||||
Where the transcript is pushed both these values, r G, r HtP(A) for the real spend.
|
|
||||||
This also serves as a DLEq proof between the key and the key image.
|
|
||||||
|
|
||||||
Checking sum(c) == H(transcript) acts a disjunction, where any one of the `c`s can be
|
|
||||||
modified to cause the intended sum, if and only if a corresponding `s` value is known.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.s);
|
|
||||||
buf.extend_from_slice(Li.compress().as_bytes());
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let Ri = (sig.s * hash_to_point(ring_member.compress().to_bytes())) + (sig.c * key_image);
|
|
||||||
buf.extend_from_slice(Ri.compress().as_bytes());
|
|
||||||
|
|
||||||
sum += sig.c;
|
|
||||||
}
|
|
||||||
sum == keccak256_to_scalar(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
|
||||||
|
|
||||||
pub use monero_mlsag as mlsag;
|
|
||||||
pub use monero_clsag as clsag;
|
|
||||||
pub use monero_borromean as borromean;
|
|
||||||
pub use monero_bulletproofs as bulletproofs;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
io::*,
|
|
||||||
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproof},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An encrypted amount.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum EncryptedAmount {
|
|
||||||
/// The original format for encrypted amounts.
|
|
||||||
Original {
|
|
||||||
/// A mask used with a mask derived from the shared secret to encrypt the amount.
|
|
||||||
mask: [u8; 32],
|
|
||||||
/// The amount, as a scalar, encrypted.
|
|
||||||
amount: [u8; 32],
|
|
||||||
},
|
|
||||||
/// The "compact" format for encrypted amounts.
|
|
||||||
Compact {
|
|
||||||
/// The amount, as a u64, encrypted.
|
|
||||||
amount: [u8; 8],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EncryptedAmount {
|
|
||||||
/// Read an EncryptedAmount from a reader.
|
|
||||||
pub fn read<R: Read>(compact: bool, r: &mut R) -> io::Result<EncryptedAmount> {
|
|
||||||
Ok(if !compact {
|
|
||||||
EncryptedAmount::Original { mask: read_bytes(r)?, amount: read_bytes(r)? }
|
|
||||||
} else {
|
|
||||||
EncryptedAmount::Compact { amount: read_bytes(r)? }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the EncryptedAmount to a writer.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
EncryptedAmount::Original { mask, amount } => {
|
|
||||||
w.write_all(mask)?;
|
|
||||||
w.write_all(amount)
|
|
||||||
}
|
|
||||||
EncryptedAmount::Compact { amount } => w.write_all(amount),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The type of the RingCT data.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub enum RctType {
|
|
||||||
/// One MLSAG for multiple inputs and Borromean range proofs.
|
|
||||||
///
|
|
||||||
/// This aligns with RCTTypeFull.
|
|
||||||
AggregateMlsagBorromean,
|
|
||||||
// One MLSAG for each input and a Borromean range proof.
|
|
||||||
///
|
|
||||||
/// This aligns with RCTTypeSimple.
|
|
||||||
MlsagBorromean,
|
|
||||||
// One MLSAG for each input and a Bulletproof.
|
|
||||||
///
|
|
||||||
/// This aligns with RCTTypeBulletproof.
|
|
||||||
MlsagBulletproofs,
|
|
||||||
/// One MLSAG for each input and a Bulletproof, yet using EncryptedAmount::Compact.
|
|
||||||
///
|
|
||||||
/// This aligns with RCTTypeBulletproof2.
|
|
||||||
MlsagBulletproofsCompactAmount,
|
|
||||||
/// One CLSAG for each input and a Bulletproof.
|
|
||||||
///
|
|
||||||
/// This aligns with RCTTypeCLSAG.
|
|
||||||
ClsagBulletproof,
|
|
||||||
/// One CLSAG for each input and a Bulletproof+.
|
|
||||||
///
|
|
||||||
/// This aligns with RCTTypeBulletproofPlus.
|
|
||||||
ClsagBulletproofPlus,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RctType> for u8 {
|
|
||||||
fn from(rct_type: RctType) -> u8 {
|
|
||||||
match rct_type {
|
|
||||||
RctType::AggregateMlsagBorromean => 1,
|
|
||||||
RctType::MlsagBorromean => 2,
|
|
||||||
RctType::MlsagBulletproofs => 3,
|
|
||||||
RctType::MlsagBulletproofsCompactAmount => 4,
|
|
||||||
RctType::ClsagBulletproof => 5,
|
|
||||||
RctType::ClsagBulletproofPlus => 6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<u8> for RctType {
|
|
||||||
type Error = ();
|
|
||||||
fn try_from(byte: u8) -> Result<Self, ()> {
|
|
||||||
Ok(match byte {
|
|
||||||
1 => RctType::AggregateMlsagBorromean,
|
|
||||||
2 => RctType::MlsagBorromean,
|
|
||||||
3 => RctType::MlsagBulletproofs,
|
|
||||||
4 => RctType::MlsagBulletproofsCompactAmount,
|
|
||||||
5 => RctType::ClsagBulletproof,
|
|
||||||
6 => RctType::ClsagBulletproofPlus,
|
|
||||||
_ => Err(())?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RctType {
|
|
||||||
/// True if this RctType uses compact encrypted amounts, false otherwise.
|
|
||||||
pub fn compact_encrypted_amounts(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
RctType::AggregateMlsagBorromean | RctType::MlsagBorromean | RctType::MlsagBulletproofs => {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
RctType::MlsagBulletproofsCompactAmount |
|
|
||||||
RctType::ClsagBulletproof |
|
|
||||||
RctType::ClsagBulletproofPlus => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True if this RctType uses a Bulletproof, false otherwise.
|
|
||||||
pub(crate) fn bulletproof(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
RctType::MlsagBulletproofs |
|
|
||||||
RctType::MlsagBulletproofsCompactAmount |
|
|
||||||
RctType::ClsagBulletproof => true,
|
|
||||||
RctType::AggregateMlsagBorromean |
|
|
||||||
RctType::MlsagBorromean |
|
|
||||||
RctType::ClsagBulletproofPlus => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True if this RctType uses a Bulletproof+, false otherwise.
|
|
||||||
pub(crate) fn bulletproof_plus(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
RctType::ClsagBulletproofPlus => true,
|
|
||||||
RctType::AggregateMlsagBorromean |
|
|
||||||
RctType::MlsagBorromean |
|
|
||||||
RctType::MlsagBulletproofs |
|
|
||||||
RctType::MlsagBulletproofsCompactAmount |
|
|
||||||
RctType::ClsagBulletproof => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The base of the RingCT data.
|
|
||||||
///
|
|
||||||
/// This excludes all proofs (which once initially verified do not need to be kept around) and
|
|
||||||
/// solely keeps data which either impacts the effects of the transactions or is needed to scan it.
|
|
||||||
///
|
|
||||||
/// The one exception for this is `pseudo_outs`, which was originally present here yet moved to
|
|
||||||
/// RctPrunable in a later hard fork (causing it to be present in both).
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct RctBase {
|
|
||||||
/// The fee used by this transaction.
|
|
||||||
pub fee: u64,
|
|
||||||
/// The re-randomized amount commitments used within inputs.
|
|
||||||
///
|
|
||||||
/// This field was deprecated and is empty for modern RctTypes.
|
|
||||||
pub pseudo_outs: Vec<EdwardsPoint>,
|
|
||||||
/// The encrypted amounts for the recipients to decrypt.
|
|
||||||
pub encrypted_amounts: Vec<EncryptedAmount>,
|
|
||||||
/// The output commitments.
|
|
||||||
pub commitments: Vec<EdwardsPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RctBase {
|
|
||||||
/// Write the RctBase.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
|
||||||
w.write_all(&[u8::from(rct_type)])?;
|
|
||||||
|
|
||||||
write_varint(&self.fee, w)?;
|
|
||||||
if rct_type == RctType::MlsagBorromean {
|
|
||||||
write_raw_vec(write_point, &self.pseudo_outs, w)?;
|
|
||||||
}
|
|
||||||
for encrypted_amount in &self.encrypted_amounts {
|
|
||||||
encrypted_amount.write(w)?;
|
|
||||||
}
|
|
||||||
write_raw_vec(write_point, &self.commitments, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a RctBase.
|
|
||||||
pub fn read<R: Read>(
|
|
||||||
inputs: usize,
|
|
||||||
outputs: usize,
|
|
||||||
r: &mut R,
|
|
||||||
) -> io::Result<Option<(RctType, RctBase)>> {
|
|
||||||
let rct_type = read_byte(r)?;
|
|
||||||
if rct_type == 0 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let rct_type =
|
|
||||||
RctType::try_from(rct_type).map_err(|()| io::Error::other("invalid RCT type"))?;
|
|
||||||
|
|
||||||
match rct_type {
|
|
||||||
RctType::AggregateMlsagBorromean | RctType::MlsagBorromean => {}
|
|
||||||
RctType::MlsagBulletproofs |
|
|
||||||
RctType::MlsagBulletproofsCompactAmount |
|
|
||||||
RctType::ClsagBulletproof |
|
|
||||||
RctType::ClsagBulletproofPlus => {
|
|
||||||
if outputs == 0 {
|
|
||||||
// Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if
|
|
||||||
// Bulletproofs are in use
|
|
||||||
// If there are Bulletproofs, there must be a matching amount of outputs, implicitly
|
|
||||||
// banning 0 outputs
|
|
||||||
// Since HF 12 (CLSAG being 13), a 2-output minimum has also been enforced
|
|
||||||
Err(io::Error::other("RCT with Bulletproofs(+) had 0 outputs"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some((
|
|
||||||
rct_type,
|
|
||||||
RctBase {
|
|
||||||
fee: read_varint(r)?,
|
|
||||||
// Only read pseudo_outs if they have yet to be moved to RctPrunable
|
|
||||||
// This would apply to AggregateMlsagBorromean and MlsagBorromean, except
|
|
||||||
// AggregateMlsagBorromean doesn't use pseudo_outs due to using the sum of the output
|
|
||||||
// commitments directly as the effective singular pseudo-out
|
|
||||||
pseudo_outs: if rct_type == RctType::MlsagBorromean {
|
|
||||||
read_raw_vec(read_point, inputs, r)?
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
},
|
|
||||||
encrypted_amounts: (0 .. outputs)
|
|
||||||
.map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r))
|
|
||||||
.collect::<Result<_, _>>()?,
|
|
||||||
commitments: read_raw_vec(read_point, outputs, r)?,
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The prunable part of the RingCT data.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum RctPrunable {
|
|
||||||
/// An aggregate MLSAG with Borromean range proofs.
|
|
||||||
AggregateMlsagBorromean {
|
|
||||||
/// The aggregate MLSAG ring signature.
|
|
||||||
mlsag: Mlsag,
|
|
||||||
/// The Borromean range proofs for each output.
|
|
||||||
borromean: Vec<BorromeanRange>,
|
|
||||||
},
|
|
||||||
/// MLSAGs with Borromean range proofs.
|
|
||||||
MlsagBorromean {
|
|
||||||
/// The MLSAG ring signatures for each input.
|
|
||||||
mlsags: Vec<Mlsag>,
|
|
||||||
/// The Borromean range proofs for each output.
|
|
||||||
borromean: Vec<BorromeanRange>,
|
|
||||||
},
|
|
||||||
/// MLSAGs with Bulletproofs.
|
|
||||||
MlsagBulletproofs {
|
|
||||||
/// The MLSAG ring signatures for each input.
|
|
||||||
mlsags: Vec<Mlsag>,
|
|
||||||
/// The re-blinded commitments for the outputs being spent.
|
|
||||||
pseudo_outs: Vec<EdwardsPoint>,
|
|
||||||
/// The aggregate Bulletproof, proving the outputs are within range.
|
|
||||||
bulletproof: Bulletproof,
|
|
||||||
},
|
|
||||||
/// MLSAGs with Bulletproofs and compact encrypted amounts.
|
|
||||||
///
|
|
||||||
/// This has an identical layout to MlsagBulletproofs and is interpreted the exact same way. It's
|
|
||||||
/// only differentiated to ensure discovery of the correct RctType.
|
|
||||||
MlsagBulletproofsCompactAmount {
|
|
||||||
/// The MLSAG ring signatures for each input.
|
|
||||||
mlsags: Vec<Mlsag>,
|
|
||||||
/// The re-blinded commitments for the outputs being spent.
|
|
||||||
pseudo_outs: Vec<EdwardsPoint>,
|
|
||||||
/// The aggregate Bulletproof, proving the outputs are within range.
|
|
||||||
bulletproof: Bulletproof,
|
|
||||||
},
|
|
||||||
/// CLSAGs with Bulletproofs(+).
|
|
||||||
Clsag {
|
|
||||||
/// The CLSAGs for each input.
|
|
||||||
clsags: Vec<Clsag>,
|
|
||||||
/// The re-blinded commitments for the outputs being spent.
|
|
||||||
pseudo_outs: Vec<EdwardsPoint>,
|
|
||||||
/// The aggregate Bulletproof(+), proving the outputs are within range.
|
|
||||||
bulletproof: Bulletproof,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RctPrunable {
|
|
||||||
/// Write the RctPrunable.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
RctPrunable::AggregateMlsagBorromean { borromean, mlsag } => {
|
|
||||||
write_raw_vec(BorromeanRange::write, borromean, w)?;
|
|
||||||
mlsag.write(w)
|
|
||||||
}
|
|
||||||
RctPrunable::MlsagBorromean { borromean, mlsags } => {
|
|
||||||
write_raw_vec(BorromeanRange::write, borromean, w)?;
|
|
||||||
write_raw_vec(Mlsag::write, mlsags, w)
|
|
||||||
}
|
|
||||||
RctPrunable::MlsagBulletproofs { bulletproof, mlsags, pseudo_outs } |
|
|
||||||
RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, mlsags, pseudo_outs } => {
|
|
||||||
if rct_type == RctType::MlsagBulletproofs {
|
|
||||||
w.write_all(&1u32.to_le_bytes())?;
|
|
||||||
} else {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
}
|
|
||||||
bulletproof.write(w)?;
|
|
||||||
|
|
||||||
write_raw_vec(Mlsag::write, mlsags, w)?;
|
|
||||||
write_raw_vec(write_point, pseudo_outs, w)
|
|
||||||
}
|
|
||||||
RctPrunable::Clsag { bulletproof, clsags, pseudo_outs } => {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
bulletproof.write(w)?;
|
|
||||||
|
|
||||||
write_raw_vec(Clsag::write, clsags, w)?;
|
|
||||||
write_raw_vec(write_point, pseudo_outs, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the RctPrunable to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self, rct_type: RctType) -> Vec<u8> {
|
|
||||||
let mut serialized = vec![];
|
|
||||||
self
|
|
||||||
.write(&mut serialized, rct_type)
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a RctPrunable.
|
|
||||||
pub fn read<R: Read>(
|
|
||||||
rct_type: RctType,
|
|
||||||
ring_length: usize,
|
|
||||||
inputs: usize,
|
|
||||||
outputs: usize,
|
|
||||||
r: &mut R,
|
|
||||||
) -> io::Result<RctPrunable> {
|
|
||||||
Ok(match rct_type {
|
|
||||||
RctType::AggregateMlsagBorromean => RctPrunable::AggregateMlsagBorromean {
|
|
||||||
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
|
|
||||||
mlsag: Mlsag::read(
|
|
||||||
ring_length,
|
|
||||||
inputs.checked_add(1).ok_or_else(|| {
|
|
||||||
io::Error::other("reading a MLSAG for more inputs than representable")
|
|
||||||
})?,
|
|
||||||
r,
|
|
||||||
)?,
|
|
||||||
},
|
|
||||||
RctType::MlsagBorromean => RctPrunable::MlsagBorromean {
|
|
||||||
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
|
|
||||||
mlsags: (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?,
|
|
||||||
},
|
|
||||||
RctType::MlsagBulletproofs | RctType::MlsagBulletproofsCompactAmount => {
|
|
||||||
let bulletproof = {
|
|
||||||
if (if rct_type == RctType::MlsagBulletproofs {
|
|
||||||
u64::from(read_u32(r)?)
|
|
||||||
} else {
|
|
||||||
read_varint(r)?
|
|
||||||
}) != 1
|
|
||||||
{
|
|
||||||
Err(io::Error::other("n bulletproofs instead of one"))?;
|
|
||||||
}
|
|
||||||
Bulletproof::read(r)?
|
|
||||||
};
|
|
||||||
let mlsags =
|
|
||||||
(0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?;
|
|
||||||
let pseudo_outs = read_raw_vec(read_point, inputs, r)?;
|
|
||||||
if rct_type == RctType::MlsagBulletproofs {
|
|
||||||
RctPrunable::MlsagBulletproofs { bulletproof, mlsags, pseudo_outs }
|
|
||||||
} else {
|
|
||||||
debug_assert_eq!(rct_type, RctType::MlsagBulletproofsCompactAmount);
|
|
||||||
RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, mlsags, pseudo_outs }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => RctPrunable::Clsag {
|
|
||||||
bulletproof: {
|
|
||||||
if read_varint::<_, u64>(r)? != 1 {
|
|
||||||
Err(io::Error::other("n bulletproofs instead of one"))?;
|
|
||||||
}
|
|
||||||
(if rct_type == RctType::ClsagBulletproof {
|
|
||||||
Bulletproof::read
|
|
||||||
} else {
|
|
||||||
Bulletproof::read_plus
|
|
||||||
})(r)?
|
|
||||||
},
|
|
||||||
clsags: (0 .. inputs).map(|_| Clsag::read(ring_length, r)).collect::<Result<_, _>>()?,
|
|
||||||
pseudo_outs: read_raw_vec(read_point, inputs, r)?,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the RctPrunable as necessary for signing the signature.
|
|
||||||
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
RctPrunable::AggregateMlsagBorromean { borromean, .. } |
|
|
||||||
RctPrunable::MlsagBorromean { borromean, .. } => {
|
|
||||||
borromean.iter().try_for_each(|rs| rs.write(w))
|
|
||||||
}
|
|
||||||
RctPrunable::MlsagBulletproofs { bulletproof, .. } |
|
|
||||||
RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, .. } |
|
|
||||||
RctPrunable::Clsag { bulletproof, .. } => bulletproof.signature_write(w),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The RingCT proofs.
|
|
||||||
///
|
|
||||||
/// This contains both the RctBase and RctPrunable structs.
|
|
||||||
///
|
|
||||||
/// The C++ codebase refers to this as rct_signatures.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct RctProofs {
|
|
||||||
/// The data necessary for handling this transaction.
|
|
||||||
pub base: RctBase,
|
|
||||||
/// The data necessary for verifying this transaction.
|
|
||||||
pub prunable: RctPrunable,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RctProofs {
|
|
||||||
/// RctType for a given RctProofs struct.
|
|
||||||
pub fn rct_type(&self) -> RctType {
|
|
||||||
match &self.prunable {
|
|
||||||
RctPrunable::AggregateMlsagBorromean { .. } => RctType::AggregateMlsagBorromean,
|
|
||||||
RctPrunable::MlsagBorromean { .. } => RctType::MlsagBorromean,
|
|
||||||
RctPrunable::MlsagBulletproofs { .. } => RctType::MlsagBulletproofs,
|
|
||||||
RctPrunable::MlsagBulletproofsCompactAmount { .. } => RctType::MlsagBulletproofsCompactAmount,
|
|
||||||
RctPrunable::Clsag { bulletproof, .. } => {
|
|
||||||
if matches!(bulletproof, Bulletproof::Original { .. }) {
|
|
||||||
RctType::ClsagBulletproof
|
|
||||||
} else {
|
|
||||||
RctType::ClsagBulletproofPlus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the RctProofs.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
let rct_type = self.rct_type();
|
|
||||||
self.base.write(w, rct_type)?;
|
|
||||||
self.prunable.write(w, rct_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the RctProofs to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = vec![];
|
|
||||||
self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a RctProofs.
|
|
||||||
pub fn read<R: Read>(
|
|
||||||
ring_length: usize,
|
|
||||||
inputs: usize,
|
|
||||||
outputs: usize,
|
|
||||||
r: &mut R,
|
|
||||||
) -> io::Result<Option<RctProofs>> {
|
|
||||||
let Some((rct_type, base)) = RctBase::read(inputs, outputs, r)? else { return Ok(None) };
|
|
||||||
Ok(Some(RctProofs {
|
|
||||||
base,
|
|
||||||
prunable: RctPrunable::read(rct_type, ring_length, inputs, outputs, r)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A pruned set of RingCT proofs.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct PrunedRctProofs {
|
|
||||||
/// The type of RctProofs this used to be.
|
|
||||||
pub rct_type: RctType,
|
|
||||||
/// The data necessary for handling this transaction.
|
|
||||||
pub base: RctBase,
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
mod transaction;
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
use curve25519_dalek::{
|
|
||||||
edwards::{CompressedEdwardsY, EdwardsPoint},
|
|
||||||
scalar::Scalar,
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
ringct::RctPrunable,
|
|
||||||
transaction::{NotPruned, Transaction, Timelock, Input},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TRANSACTIONS: &str = include_str!("./vectors/transactions.json");
|
|
||||||
const CLSAG_TX: &str = include_str!("./vectors/clsag_tx.json");
|
|
||||||
const RING_DATA: &str = include_str!("./vectors/ring_data.json");
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct Vector {
|
|
||||||
id: String,
|
|
||||||
hex: String,
|
|
||||||
signature_hash: String,
|
|
||||||
tx: Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tx_vectors() -> Vec<Vector> {
|
|
||||||
serde_json::from_str(TRANSACTIONS).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn point(hex: &Value) -> EdwardsPoint {
|
|
||||||
CompressedEdwardsY(hex::decode(hex.as_str().unwrap()).unwrap().try_into().unwrap())
|
|
||||||
.decompress()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scalar(hex: &Value) -> Scalar {
|
|
||||||
Scalar::from_canonical_bytes(hex::decode(hex.as_str().unwrap()).unwrap().try_into().unwrap())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn point_vector(val: &Value) -> Vec<EdwardsPoint> {
|
|
||||||
let mut v = vec![];
|
|
||||||
for hex in val.as_array().unwrap() {
|
|
||||||
v.push(point(hex));
|
|
||||||
}
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scalar_vector(val: &Value) -> Vec<Scalar> {
|
|
||||||
let mut v = vec![];
|
|
||||||
for hex in val.as_array().unwrap() {
|
|
||||||
v.push(scalar(hex));
|
|
||||||
}
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
for v in tx_vectors() {
|
|
||||||
let tx =
|
|
||||||
Transaction::<NotPruned>::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap();
|
|
||||||
|
|
||||||
// check version
|
|
||||||
assert_eq!(tx.version(), v.tx["version"]);
|
|
||||||
|
|
||||||
// check unlock time
|
|
||||||
match tx.prefix().additional_timelock {
|
|
||||||
Timelock::None => assert_eq!(0, v.tx["unlock_time"]),
|
|
||||||
Timelock::Block(h) => assert_eq!(h, v.tx["unlock_time"]),
|
|
||||||
Timelock::Time(t) => assert_eq!(t, v.tx["unlock_time"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
// check inputs
|
|
||||||
let inputs = v.tx["vin"].as_array().unwrap();
|
|
||||||
assert_eq!(tx.prefix().inputs.len(), inputs.len());
|
|
||||||
for (i, input) in tx.prefix().inputs.iter().enumerate() {
|
|
||||||
match input {
|
|
||||||
Input::Gen(h) => assert_eq!(*h, inputs[i]["gen"]["height"]),
|
|
||||||
Input::ToKey { amount, key_offsets, key_image } => {
|
|
||||||
let key = &inputs[i]["key"];
|
|
||||||
assert_eq!(amount.unwrap_or(0), key["amount"]);
|
|
||||||
assert_eq!(*key_image, point(&key["k_image"]));
|
|
||||||
assert_eq!(key_offsets, key["key_offsets"].as_array().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check outputs
|
|
||||||
let outputs = v.tx["vout"].as_array().unwrap();
|
|
||||||
assert_eq!(tx.prefix().outputs.len(), outputs.len());
|
|
||||||
for (i, output) in tx.prefix().outputs.iter().enumerate() {
|
|
||||||
assert_eq!(output.amount.unwrap_or(0), outputs[i]["amount"]);
|
|
||||||
if output.view_tag.is_some() {
|
|
||||||
assert_eq!(output.key, point(&outputs[i]["target"]["tagged_key"]["key"]).compress());
|
|
||||||
let view_tag =
|
|
||||||
hex::decode(outputs[i]["target"]["tagged_key"]["view_tag"].as_str().unwrap()).unwrap();
|
|
||||||
assert_eq!(view_tag.len(), 1);
|
|
||||||
assert_eq!(output.view_tag.unwrap(), view_tag[0]);
|
|
||||||
} else {
|
|
||||||
assert_eq!(output.key, point(&outputs[i]["target"]["key"]).compress());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check extra
|
|
||||||
assert_eq!(tx.prefix().extra, v.tx["extra"].as_array().unwrap().as_slice());
|
|
||||||
|
|
||||||
match &tx {
|
|
||||||
Transaction::V1 { signatures, .. } => {
|
|
||||||
// check signatures for v1 txs
|
|
||||||
let sigs_array = v.tx["signatures"].as_array().unwrap();
|
|
||||||
for (i, sig) in signatures.iter().enumerate() {
|
|
||||||
let tx_sig = hex::decode(sigs_array[i].as_str().unwrap()).unwrap();
|
|
||||||
for (i, sig) in sig.sigs.iter().enumerate() {
|
|
||||||
let start = i * 64;
|
|
||||||
let c: [u8; 32] = tx_sig[start .. (start + 32)].try_into().unwrap();
|
|
||||||
let s: [u8; 32] = tx_sig[(start + 32) .. (start + 64)].try_into().unwrap();
|
|
||||||
assert_eq!(sig.c, Scalar::from_canonical_bytes(c).unwrap());
|
|
||||||
assert_eq!(sig.s, Scalar::from_canonical_bytes(s).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Transaction::V2 { proofs: None, .. } => assert_eq!(v.tx["rct_signatures"]["type"], 0),
|
|
||||||
Transaction::V2 { proofs: Some(proofs), .. } => {
|
|
||||||
// check rct signatures
|
|
||||||
let rct = &v.tx["rct_signatures"];
|
|
||||||
assert_eq!(u8::from(proofs.rct_type()), rct["type"]);
|
|
||||||
|
|
||||||
assert_eq!(proofs.base.fee, rct["txnFee"]);
|
|
||||||
assert_eq!(proofs.base.commitments, point_vector(&rct["outPk"]));
|
|
||||||
let ecdh_info = rct["ecdhInfo"].as_array().unwrap();
|
|
||||||
assert_eq!(proofs.base.encrypted_amounts.len(), ecdh_info.len());
|
|
||||||
for (i, ecdh) in proofs.base.encrypted_amounts.iter().enumerate() {
|
|
||||||
let mut buf = vec![];
|
|
||||||
ecdh.write(&mut buf).unwrap();
|
|
||||||
assert_eq!(buf, hex::decode(ecdh_info[i]["amount"].as_str().unwrap()).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// check ringct prunable
|
|
||||||
match &proofs.prunable {
|
|
||||||
RctPrunable::Clsag { bulletproof: _, clsags, pseudo_outs } => {
|
|
||||||
// check bulletproofs
|
|
||||||
/* TODO
|
|
||||||
for (i, bp) in bulletproofs.iter().enumerate() {
|
|
||||||
match bp {
|
|
||||||
Bulletproof::Original(o) => {
|
|
||||||
let bps = v.tx["rctsig_prunable"]["bp"].as_array().unwrap();
|
|
||||||
assert_eq!(bulletproofs.len(), bps.len());
|
|
||||||
assert_eq!(o.A, point(&bps[i]["A"]));
|
|
||||||
assert_eq!(o.S, point(&bps[i]["S"]));
|
|
||||||
assert_eq!(o.T1, point(&bps[i]["T1"]));
|
|
||||||
assert_eq!(o.T2, point(&bps[i]["T2"]));
|
|
||||||
assert_eq!(o.taux, scalar(&bps[i]["taux"]));
|
|
||||||
assert_eq!(o.mu, scalar(&bps[i]["mu"]));
|
|
||||||
assert_eq!(o.L, point_vector(&bps[i]["L"]));
|
|
||||||
assert_eq!(o.R, point_vector(&bps[i]["R"]));
|
|
||||||
assert_eq!(o.a, scalar(&bps[i]["a"]));
|
|
||||||
assert_eq!(o.b, scalar(&bps[i]["b"]));
|
|
||||||
assert_eq!(o.t, scalar(&bps[i]["t"]));
|
|
||||||
}
|
|
||||||
Bulletproof::Plus(p) => {
|
|
||||||
let bps = v.tx["rctsig_prunable"]["bpp"].as_array().unwrap();
|
|
||||||
assert_eq!(bulletproofs.len(), bps.len());
|
|
||||||
assert_eq!(p.A, point(&bps[i]["A"]));
|
|
||||||
assert_eq!(p.A1, point(&bps[i]["A1"]));
|
|
||||||
assert_eq!(p.B, point(&bps[i]["B"]));
|
|
||||||
assert_eq!(p.r1, scalar(&bps[i]["r1"]));
|
|
||||||
assert_eq!(p.s1, scalar(&bps[i]["s1"]));
|
|
||||||
assert_eq!(p.d1, scalar(&bps[i]["d1"]));
|
|
||||||
assert_eq!(p.L, point_vector(&bps[i]["L"]));
|
|
||||||
assert_eq!(p.R, point_vector(&bps[i]["R"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// check clsags
|
|
||||||
let cls = v.tx["rctsig_prunable"]["CLSAGs"].as_array().unwrap();
|
|
||||||
for (i, cl) in clsags.iter().enumerate() {
|
|
||||||
assert_eq!(cl.D, point(&cls[i]["D"]));
|
|
||||||
assert_eq!(cl.c1, scalar(&cls[i]["c1"]));
|
|
||||||
assert_eq!(cl.s, scalar_vector(&cls[i]["s"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// check pseudo outs
|
|
||||||
assert_eq!(pseudo_outs, &point_vector(&v.tx["rctsig_prunable"]["pseudoOuts"]));
|
|
||||||
}
|
|
||||||
// TODO: Add
|
|
||||||
_ => panic!("non-null/CLSAG test vector"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check serialized hex
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
tx.write(&mut buf).unwrap();
|
|
||||||
let serialized_tx = hex::encode(&buf);
|
|
||||||
assert_eq!(serialized_tx, v.hex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn signature_hash() {
|
|
||||||
for v in tx_vectors() {
|
|
||||||
let tx = Transaction::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap();
|
|
||||||
// check for signature hashes
|
|
||||||
if let Some(sig_hash) = tx.signature_hash() {
|
|
||||||
assert_eq!(sig_hash, hex::decode(v.signature_hash.clone()).unwrap().as_slice());
|
|
||||||
} else {
|
|
||||||
// make sure it is a miner tx.
|
|
||||||
assert!(matches!(tx.prefix().inputs[0], Input::Gen(_)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hash() {
|
|
||||||
for v in &tx_vectors() {
|
|
||||||
let tx = Transaction::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap();
|
|
||||||
assert_eq!(tx.hash(), hex::decode(v.id.clone()).unwrap().as_slice());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn clsag() {
|
|
||||||
/*
|
|
||||||
// following keys belong to the wallet that created the CLSAG_TX, and to the
|
|
||||||
// CLSAG_TX itself and here for debug purposes in case this test unexpectedly fails some day.
|
|
||||||
let view_key = "9df81dd2e369004d3737850e4f0abaf2111720f270b174acf8e08547e41afb0b";
|
|
||||||
let spend_key = "25f7339ce03a0206129c0bdd78396f80bf28183ccd16084d4ab1cbaf74f0c204";
|
|
||||||
let tx_key = "650c8038e5c6f1c533cacc1713ac27ef3ec70d7feedde0c5b37556d915b4460c";
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct TxData {
|
|
||||||
hex: String,
|
|
||||||
tx: Value,
|
|
||||||
}
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct OutData {
|
|
||||||
key: Value,
|
|
||||||
mask: Value,
|
|
||||||
}
|
|
||||||
let tx_data = serde_json::from_str::<TxData>(CLSAG_TX).unwrap();
|
|
||||||
let out_data = serde_json::from_str::<Vec<Vec<OutData>>>(RING_DATA).unwrap();
|
|
||||||
let tx =
|
|
||||||
Transaction::<NotPruned>::read(&mut hex::decode(tx_data.hex).unwrap().as_slice()).unwrap();
|
|
||||||
|
|
||||||
// gather rings
|
|
||||||
let mut rings = vec![];
|
|
||||||
for data in out_data {
|
|
||||||
let mut ring = vec![];
|
|
||||||
for out in &data {
|
|
||||||
ring.push([point(&out.key), point(&out.mask)]);
|
|
||||||
}
|
|
||||||
rings.push(ring)
|
|
||||||
}
|
|
||||||
|
|
||||||
// gather key images
|
|
||||||
let mut key_images = vec![];
|
|
||||||
let inputs = tx_data.tx["vin"].as_array().unwrap();
|
|
||||||
for input in inputs {
|
|
||||||
key_images.push(point(&input["key"]["k_image"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// gather pseudo_outs
|
|
||||||
let mut pseudo_outs = vec![];
|
|
||||||
let pouts = tx_data.tx["rctsig_prunable"]["pseudoOuts"].as_array().unwrap();
|
|
||||||
for po in pouts {
|
|
||||||
pseudo_outs.push(point(po));
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify clsags
|
|
||||||
match tx {
|
|
||||||
Transaction::V2 { proofs: Some(ref proofs), .. } => match &proofs.prunable {
|
|
||||||
RctPrunable::Clsag { bulletproof: _, clsags, .. } => {
|
|
||||||
for (i, cls) in clsags.iter().enumerate() {
|
|
||||||
cls
|
|
||||||
.verify(&rings[i], &key_images[i], &pseudo_outs[i], &tx.signature_hash().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: Add
|
|
||||||
_ => panic!("non-CLSAG test vector"),
|
|
||||||
},
|
|
||||||
// TODO: Add
|
|
||||||
_ => panic!("non-CLSAG test vector"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
{
|
|
||||||
"hex": "020002020010020102010101010302010c0201060103d8c6f077bb201ffdc16407df206cb5962ec635a4a4c9cd7551b88698d1bef497020010000402040801010303030101020104018267c18a435f4a5dea50ad0f10755a4fd7783340beb3a3903a67fa14938edf420200039716cdbae38def9a74e7df5402c108270a1d5fc87c7e5ebaaaed68aae77701e3cf0003082e27ca8af2b9e3004156c152aa98503b548b1591fdcd839ab550612ae6c9dc7e2c01a57c93fb0ca77ab96b7dfd7380c4842d1e58c055430e0d425cd1c76c578cca390209019519f8c1ce5e20300680e5a0da09acd081c0dd2c7178a341382720ada87588a96ac5cff1623fd2e4aaf56ed395a325393fbd950428a3ff7e6dc6c559669c8d5e8fb80d5e979c8a81c89754201d4bd094c37c143759260e282555dfed3100013256ca0156c1c34dc569565039c27f784b45ec50ba816f69b54ae3df98d841070f51aec2a8afd4991d5bbf50b785d0bdc2a6491c5ab45795d7ce3b08d63282907c52f9951e711cb6a2cba1aba1f7849a669345711263cc736e2d4e1c7308c5e7cb97e948ed647f89fc9869fb9c9a5a742e5e7be419cce7a5e99a5b21cb491f00003ec1da7e8cec39b709d46fab65f59f5f6147c1e4429d18d8bf6e3e62639102a300ce6006a20403ef021a197b6c632ac280e674c7aad08290424271dec4de010710ee7895389150dd15017cfd5f47ea9dddd11e218251433906f62aff6b8cb2b5f8cca25add297da40d7cddbea718703ff9ad3795fcdc172a34c73179326c16f5274de69073281f3276d800fe7fbd01a07d14a42ce367c32727a9f0bc8c8d6ab4b3b17dd981bdc522595fc1bfe83ad3976876fb3bb2e4bd4392ac1a94ac22cbcc326ede82d1af2f1ec9d4ac596b22d035c7f1ac11d8ace7c5a70b30e39596ded794077ae55144e3f4b0c17cbc4f5a960129eb5321077bb7e2b9e4621e17fbcf2960abae1e1a9f89af21cc2fcce410a839186b8da92966415d6dd3ad772d652cbe075af46b97ae7062ccbaa328e371a351492f6860832c5bfdd7b77e8611b7441ecfa0967e66c13cb9ab348bf78a15bbd2d9bec6b8ec5cdd5f84a91580758247da84afed22ec2cf89d632e406fdc927e48ebfaacd0a0b715a968c9cfc74fff611f4cda4b6cb9eb1e044a71c58a832c5ae7551833c0ba2ab6f9d1e466e5757c230157cd3099686bf89e8f9eb822ea702e13e38f669603dd3c7c8be90daf192de689ab2078d16cf489f3782e70469fbe01f918297e0db6cef3bf48e0293b6856d348fda3a2d76bf899432acef74aa42961be28635d1899509b9d368bc42a18e08d2b94b055da149139c347f7c0b2a381dfaa12aaaabe076f38fe12372d1ba17cd0d808ed5b4b911f8cee2e45841a4c879f40968e455ba5a796b27c968be0f7e88daf0b766fcf2c5986fbe14b2e0433cecb04af100ec81d03e2875d25483d0a9dc9dc0a42150a64e894af1655e9ab99f629826f63c01e44b366c5fe2959c7396450360a3156ad081764b5904a7654fe82a2b1d52db46361c0b08dfeee383165641e6e0e5733e5fb99fc8c75ba5cf230518b1e384d4441251840e810aed950eb27899809711d42c54f8fc0647537e249e510738412c399b915ff923e9209cdd12820720b8b07086f3361d6b95934f994a8ac4fb6a9598f11d54bbbcfc33e71b9f73570012b3520914dfab3f3fe15abad981d8ed71dab71ac8f45f187f62ad440a83d000e08fc039ece25e7eadd0ce169ccda8182321cd73eba6f6d0e4f482a061eb4190fe4051e6988a47165cb2cf39973b1a555cc92d662f4e856a91c0cd51a486b960cfc850c4fc854f9a4aade4336942cb50cb50ae3bc31d3da50b719196d5fd40f02b1addad16de443e825bf7177beaac79adc6b198115f408a391a94a8517b7e50fd57663df52309c0a00b0b61373f895206771be8b185c54da6f805b561264aa019ef3bd1dcded26fc45a6a0e39cbb7bc6a7025ab858bc8e54a99da3aedce68f00bacc83a7eb3553ac626881188329b6ba86a53aaaaed9bd9efb0528f08c649c093f005dd0fa9620b0a40fc3f248c1d0edb8f70ff05c7254de0f8faab8315443021b3d279f5a4218c3126dee5d6eceae1c49eabdd04d8a0cdb6814c422b3ea69b3be3794f42081e65dc47b1d2fc2f5705cef816596416c373bd60abc4ff06b3f02ef34dc290f987607bdb16c1650307ea3bc0fc7a62ce86e7129293d7530c3cf09dc731e22c18daec3c639575421b079fa57be56693278125b2aa50c299ac4f8020714c6ac666b7fb7471c63adda93f1fa6733729f7b6e326ac04744f9c3223d0456ee515d0bfe27101f907cca958dddb90717bed5229c1a02928fab9e7be4e4012c96d3acda0ebca72e63f41efdad5c9baa19bffd1216e4c3e2e5564e823b57054a3a2cf2c3318f214d23f24304655e73d5001518633757f6cbe6711f2a5f2601df20a753caaa87a32fe627b6ce7573ce77957c7b6401959824fd49bc7063670fb18fcc1f2de113affd868eb76c7fbe12997024dc493b6a26563a80574a52760a7b384fd2f9d23d8dfe4d226b15086751d4f383d4bca7cf080fd471b8a218b709b539f4e5417677f43627ef06b70c24edacce80bdd10ca2ac9af8aa3f6453cc08da75ee99409447225843c143fca551167a4aa5fd2354a5420c35c0006731950d6c356218d8cf365e084d9bb52c793322aa2d8d05c4164d9ffe81ce09e4f17802efa7461d375a5cff4c17ab0cdc5767a8f7d34091921fd4620660470ea9305f00dd9e6ee5ca4054ac0b36d4e2b58006224559cc19a3a4e48f66aa596295541007f2524b2198f3c0c688fbbc38590f59674b25e528ac2115a0f7da805d9c5810065f95c7c7ece23d2de922e55a77f967baab6d9db543e49734a8c4bc23c5ae640edb904851b4856c5a1ce4729957f4d000e70cb88c56d80bf6e693a5c67d5661911374d7aa7f6e6f4a5b340a9954d9cf8bd5d2f4b4a37f946e15bca800978ae745eec2096b3def10f9703a6e2040df0d8a89bf1562bb29d3a13df2f9a77c3e064e",
|
|
||||||
"tx": {
|
|
||||||
"version": 2,
|
|
||||||
"unlock_time": 0,
|
|
||||||
"vin": [ {
|
|
||||||
"key": {
|
|
||||||
"amount": 0,
|
|
||||||
"key_offsets": [ 2, 1, 2, 1, 1, 1, 1, 3, 2, 1, 12, 2, 1, 6, 1, 3],
|
|
||||||
"k_image": "d8c6f077bb201ffdc16407df206cb5962ec635a4a4c9cd7551b88698d1bef497"
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
"key": {
|
|
||||||
"amount": 0,
|
|
||||||
"key_offsets": [ 0, 4, 2, 4, 8, 1, 1, 3, 3, 3, 1, 1, 2, 1, 4, 1
|
|
||||||
],
|
|
||||||
"k_image": "8267c18a435f4a5dea50ad0f10755a4fd7783340beb3a3903a67fa14938edf42"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"vout": [ {
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"tagged_key": {
|
|
||||||
"key": "9716cdbae38def9a74e7df5402c108270a1d5fc87c7e5ebaaaed68aae77701e3",
|
|
||||||
"view_tag": "cf"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"tagged_key": {
|
|
||||||
"key": "082e27ca8af2b9e3004156c152aa98503b548b1591fdcd839ab550612ae6c9dc",
|
|
||||||
"view_tag": "7e"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extra": [ 1, 165, 124, 147, 251, 12, 167, 122, 185, 107, 125, 253, 115, 128, 196, 132, 45, 30, 88, 192, 85, 67, 14, 13, 66, 92, 209, 199, 108, 87, 140, 202, 57, 2, 9, 1, 149, 25, 248, 193, 206, 94, 32, 48
|
|
||||||
],
|
|
||||||
"rct_signatures": {
|
|
||||||
"type": 6,
|
|
||||||
"txnFee": 2605200000,
|
|
||||||
"ecdhInfo": [ {
|
|
||||||
"amount": "acd081c0dd2c7178"
|
|
||||||
}, {
|
|
||||||
"amount": "a341382720ada875"
|
|
||||||
}],
|
|
||||||
"outPk": [ "88a96ac5cff1623fd2e4aaf56ed395a325393fbd950428a3ff7e6dc6c559669c", "8d5e8fb80d5e979c8a81c89754201d4bd094c37c143759260e282555dfed3100"]
|
|
||||||
},
|
|
||||||
"rctsig_prunable": {
|
|
||||||
"nbp": 1,
|
|
||||||
"bpp": [ {
|
|
||||||
"A": "3256ca0156c1c34dc569565039c27f784b45ec50ba816f69b54ae3df98d84107",
|
|
||||||
"A1": "0f51aec2a8afd4991d5bbf50b785d0bdc2a6491c5ab45795d7ce3b08d6328290",
|
|
||||||
"B": "7c52f9951e711cb6a2cba1aba1f7849a669345711263cc736e2d4e1c7308c5e7",
|
|
||||||
"r1": "cb97e948ed647f89fc9869fb9c9a5a742e5e7be419cce7a5e99a5b21cb491f00",
|
|
||||||
"s1": "003ec1da7e8cec39b709d46fab65f59f5f6147c1e4429d18d8bf6e3e62639102",
|
|
||||||
"d1": "a300ce6006a20403ef021a197b6c632ac280e674c7aad08290424271dec4de01",
|
|
||||||
"L": [ "10ee7895389150dd15017cfd5f47ea9dddd11e218251433906f62aff6b8cb2b5", "f8cca25add297da40d7cddbea718703ff9ad3795fcdc172a34c73179326c16f5", "274de69073281f3276d800fe7fbd01a07d14a42ce367c32727a9f0bc8c8d6ab4", "b3b17dd981bdc522595fc1bfe83ad3976876fb3bb2e4bd4392ac1a94ac22cbcc", "326ede82d1af2f1ec9d4ac596b22d035c7f1ac11d8ace7c5a70b30e39596ded7", "94077ae55144e3f4b0c17cbc4f5a960129eb5321077bb7e2b9e4621e17fbcf29", "60abae1e1a9f89af21cc2fcce410a839186b8da92966415d6dd3ad772d652cbe"
|
|
||||||
],
|
|
||||||
"R": [ "5af46b97ae7062ccbaa328e371a351492f6860832c5bfdd7b77e8611b7441ecf", "a0967e66c13cb9ab348bf78a15bbd2d9bec6b8ec5cdd5f84a91580758247da84", "afed22ec2cf89d632e406fdc927e48ebfaacd0a0b715a968c9cfc74fff611f4c", "da4b6cb9eb1e044a71c58a832c5ae7551833c0ba2ab6f9d1e466e5757c230157", "cd3099686bf89e8f9eb822ea702e13e38f669603dd3c7c8be90daf192de689ab", "2078d16cf489f3782e70469fbe01f918297e0db6cef3bf48e0293b6856d348fd", "a3a2d76bf899432acef74aa42961be28635d1899509b9d368bc42a18e08d2b94"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"CLSAGs": [ {
|
|
||||||
"s": [ "b055da149139c347f7c0b2a381dfaa12aaaabe076f38fe12372d1ba17cd0d808", "ed5b4b911f8cee2e45841a4c879f40968e455ba5a796b27c968be0f7e88daf0b", "766fcf2c5986fbe14b2e0433cecb04af100ec81d03e2875d25483d0a9dc9dc0a", "42150a64e894af1655e9ab99f629826f63c01e44b366c5fe2959c7396450360a", "3156ad081764b5904a7654fe82a2b1d52db46361c0b08dfeee383165641e6e0e", "5733e5fb99fc8c75ba5cf230518b1e384d4441251840e810aed950eb27899809", "711d42c54f8fc0647537e249e510738412c399b915ff923e9209cdd12820720b", "8b07086f3361d6b95934f994a8ac4fb6a9598f11d54bbbcfc33e71b9f7357001", "2b3520914dfab3f3fe15abad981d8ed71dab71ac8f45f187f62ad440a83d000e", "08fc039ece25e7eadd0ce169ccda8182321cd73eba6f6d0e4f482a061eb4190f", "e4051e6988a47165cb2cf39973b1a555cc92d662f4e856a91c0cd51a486b960c", "fc850c4fc854f9a4aade4336942cb50cb50ae3bc31d3da50b719196d5fd40f02", "b1addad16de443e825bf7177beaac79adc6b198115f408a391a94a8517b7e50f", "d57663df52309c0a00b0b61373f895206771be8b185c54da6f805b561264aa01", "9ef3bd1dcded26fc45a6a0e39cbb7bc6a7025ab858bc8e54a99da3aedce68f00", "bacc83a7eb3553ac626881188329b6ba86a53aaaaed9bd9efb0528f08c649c09"],
|
|
||||||
"c1": "3f005dd0fa9620b0a40fc3f248c1d0edb8f70ff05c7254de0f8faab831544302",
|
|
||||||
"D": "1b3d279f5a4218c3126dee5d6eceae1c49eabdd04d8a0cdb6814c422b3ea69b3"
|
|
||||||
}, {
|
|
||||||
"s": [ "be3794f42081e65dc47b1d2fc2f5705cef816596416c373bd60abc4ff06b3f02", "ef34dc290f987607bdb16c1650307ea3bc0fc7a62ce86e7129293d7530c3cf09", "dc731e22c18daec3c639575421b079fa57be56693278125b2aa50c299ac4f802", "0714c6ac666b7fb7471c63adda93f1fa6733729f7b6e326ac04744f9c3223d04", "56ee515d0bfe27101f907cca958dddb90717bed5229c1a02928fab9e7be4e401", "2c96d3acda0ebca72e63f41efdad5c9baa19bffd1216e4c3e2e5564e823b5705", "4a3a2cf2c3318f214d23f24304655e73d5001518633757f6cbe6711f2a5f2601", "df20a753caaa87a32fe627b6ce7573ce77957c7b6401959824fd49bc7063670f", "b18fcc1f2de113affd868eb76c7fbe12997024dc493b6a26563a80574a52760a", "7b384fd2f9d23d8dfe4d226b15086751d4f383d4bca7cf080fd471b8a218b709", "b539f4e5417677f43627ef06b70c24edacce80bdd10ca2ac9af8aa3f6453cc08", "da75ee99409447225843c143fca551167a4aa5fd2354a5420c35c0006731950d", "6c356218d8cf365e084d9bb52c793322aa2d8d05c4164d9ffe81ce09e4f17802", "efa7461d375a5cff4c17ab0cdc5767a8f7d34091921fd4620660470ea9305f00", "dd9e6ee5ca4054ac0b36d4e2b58006224559cc19a3a4e48f66aa596295541007", "f2524b2198f3c0c688fbbc38590f59674b25e528ac2115a0f7da805d9c581006"],
|
|
||||||
"c1": "5f95c7c7ece23d2de922e55a77f967baab6d9db543e49734a8c4bc23c5ae640e",
|
|
||||||
"D": "db904851b4856c5a1ce4729957f4d000e70cb88c56d80bf6e693a5c67d566191"
|
|
||||||
}],
|
|
||||||
"pseudoOuts": [ "1374d7aa7f6e6f4a5b340a9954d9cf8bd5d2f4b4a37f946e15bca800978ae745", "eec2096b3def10f9703a6e2040df0d8a89bf1562bb29d3a13df2f9a77c3e064e"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
[
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "a1abc026eb4a18ca197ca7dbd32f7a4e66cda075a7c07ee6cbe68639a4b4ee46",
|
|
||||||
"mask": "48d7f0b8796720c7edef5e3797135b3e5ad2ae23db1d934bcf6d6bc396b8ed47"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "a374121e22ed620248c970e7f32ea7598b054f73c1edec33c4e1b18a73c35c14",
|
|
||||||
"mask": "15beeeedc9b33615097e0fac0acc6a0984e139fa2b4196896877a8cc3ebc3590"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "e2ac4d36f9567092563a09c7a19c5e21c39598f5d9d9dd8733b61cebb3ea8662",
|
|
||||||
"mask": "3d9105f85f9edd3f7f72b62385bb9a42d549331d3babea6cf73bbbcde8e4f53c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "68c08bbbfdb3ad736dfed5854264a3b410de40d8f3d02b22f5cf75f69f6e2e1f",
|
|
||||||
"mask": "36c39958ddcad401d85d63883da510505650321ad7a26859e8b1b6c28204d274"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "7b8b580f7a2288040a0755810c5708c5a8277d139762545082785260275678e4",
|
|
||||||
"mask": "498105ec1dc7559becfb833140c5049382b846eff812616a2414494d7a46930d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "348d9be3f2b42686c2a919ba1515c5a540c5ffb4c1762e4a371b42643ff69b3b",
|
|
||||||
"mask": "eeca9ed04ba72a89dbd85564cf3084daad577634db09d048895524f1ded26b19"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "91a59666453bcc55d2a02480dfe2029082e24548cdfd7d614be31657fdd75357",
|
|
||||||
"mask": "ae7f14cbb31d24b727d8680fbd03bcc177fc67b982edeca54e6b2b47d6b8d012"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "9868cb5201d4b00e5a3552a7f485662dfb3ca74b79f6bd069ee0a4650597abbc",
|
|
||||||
"mask": "570e3b126e429022177d22fd09d73c6950676c82a4872addb3afa950646c5f1d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "56d05fced0eb9dda981a26fdd4170f46de2b0a35c70f02ceae23ad9f2ed8a5b0",
|
|
||||||
"mask": "a0e20ecd8526bd2a640c4df42c187fcf75d05660ba61262c93b19384b8fad49b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "9e82f65349da1e0dacf5d96a9c0f80c0c5fd0fc2437cafbcc38b2f20e721abc5",
|
|
||||||
"mask": "e83344061c0632631eec627bb2103898cfc230b35e0177681e48f0ee4b6d37c8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "2590a255607ab619fcd62142f4b002818f2d55dbb5b8665500854203b83e5c86",
|
|
||||||
"mask": "e9c103485b3f4dadab560e8efc67c594ba11f16513685f0faff78c6fdf4de061"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "c0e22332d897f0637440ad151089652e59dcbf27dc84b11c2efbe686a9e7afb5",
|
|
||||||
"mask": "363d5dcbc765854e830dc52762e24f71d7c85f6095227551f3ef6ada6aa25964"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "360e4efb484e8d419bdda5f581703de716671e3516d1c9deb97204f9b4c9c0d4",
|
|
||||||
"mask": "29ef141fa24ef86af35af48094928392543a9e7e7726ae92a9da322178e680ad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "5bb515d131f03bbb3be4e710b83589f62f07f185b9ad344095df47092f41b8e0",
|
|
||||||
"mask": "94fd6083b669533eebfa49a1cb47b94555e8be7d5f84573354b0201229d07bed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "5ce647c3017ec3c36a2385e2b11fb9a452a5766987d80531bec75952924ed896",
|
|
||||||
"mask": "8f61d7be3b4f2252810fbade3bbac970ccff55c453e34405836545f3e49be6f5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "dbc787f7ca41996a981a0ebb498a8d565dfa62a3b3b169c4c3018fff2233a757",
|
|
||||||
"mask": "9bb749be705747d9c28168c0446d589b3ac18949fa0087e230805aaff5a9982f"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "d10621b38fbc5237061b2d3503866f0be46aaa0694c9f9d747f7ed19acebe8ef",
|
|
||||||
"mask": "a1a7a42155f0abff0353a6008eda2a9b16d9ffcf7584a38933cce3e3976987cd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "a9afb71ae2db057049131df856d246f7088a656cc85297ce7e1ef339bd6e0c96",
|
|
||||||
"mask": "96e9dc7a96a19c9ebaeb33ab94e7e9d86d88df1c1b11006b297b74f529f37f5a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "68c08bbbfdb3ad736dfed5854264a3b410de40d8f3d02b22f5cf75f69f6e2e1f",
|
|
||||||
"mask": "36c39958ddcad401d85d63883da510505650321ad7a26859e8b1b6c28204d274"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "74193737897162c8b2c380ff34674e3bfbfb2ac7e1c7aacbb13f2a3a8fb2b043",
|
|
||||||
"mask": "8157e47f9998f4afdce72a328eb9e897a57a5819b838ed1b517ea2c938e0c94f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "96e002055aafbfdd1136cc587543e5c0e51da0d9682879c107abab3cdcdb9479",
|
|
||||||
"mask": "f76929f6dba6d75bec713a02677aa7ad39dd4319077bfa7189fe65fe86b2ee9a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "2a72f3b2cb3e10727fbfc09d2c726763000a92f77f2f000c63dee714a6c7424d",
|
|
||||||
"mask": "db459ca84da12ebab294b31961838c43cee1868f0690d143c93da1f2f825d07f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "797f5f3a30ce8d4b19305ca9d8193033d649f0a74705203da9f3f106ad60dfb4",
|
|
||||||
"mask": "39339ac52a1194790b1bb5db0b119d403a1d5dcc4db4f8819fca4d425d5b2614"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "b0c42947607815eba320f97e7c9ecd092fe187fb67d7263540015e6308f6dc1a",
|
|
||||||
"mask": "6b92c8c269319192298307feb26a7b64fb78d877ac2e49a594650227f26e64bc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "59015cfd533a742857454dce9d82846fce08ab7d96c5583640cf6e38ecf0445e",
|
|
||||||
"mask": "cf375f037e253ab6f52699fbba73f796ee2140e546710a1faa3c9f09b4f570ac"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "c0e22332d897f0637440ad151089652e59dcbf27dc84b11c2efbe686a9e7afb5",
|
|
||||||
"mask": "363d5dcbc765854e830dc52762e24f71d7c85f6095227551f3ef6ada6aa25964"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "360e4efb484e8d419bdda5f581703de716671e3516d1c9deb97204f9b4c9c0d4",
|
|
||||||
"mask": "29ef141fa24ef86af35af48094928392543a9e7e7726ae92a9da322178e680ad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "92619df80e988c0b2dfb63dd6324ff2979ca319bf8200260b28944753dda4ac1",
|
|
||||||
"mask": "0a574b0aca86da38dd7aeb58d92550dc558c680deaa63c69e31e9a78e88a3559"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "0ac7e630a04be92b1f3c821c50ec80a2813f7bee4c1ab117967bc26263d4fd84",
|
|
||||||
"mask": "ed0bd4d707ab3deaf18437ae9d945da2d3f2c6e758068ce57972d676da2a24bf"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "b97300cdb6ef63a6990686521138b5c7c80cf6c9a8844518352f3ef1130d413d",
|
|
||||||
"mask": "690c312586bbdf123d9e34ad7955e1c2ae5259cd3effd0b08b19cb556d65ec25"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "1a62237b77e28713e5a47129f1ba18be27a5139d6f1e6d6d38c78705143b3ea5",
|
|
||||||
"mask": "39f6ba6d816695f20212042b1048301cd637161f685d7c2b61379b907b7b4c59"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ffca492152d8206bb7f215d2408669856203edffd424f4fc6a0304def2195717",
|
|
||||||
"mask": "cd7684b7c32531b363784d86bee71731c113c545c67103ec1265c362de7e5555"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "373a2ace627debaf8bfd493155fd3c00c5c2fc164400ec22e79ee79a1ac487c4",
|
|
||||||
"hex": "02f78dae0101ffbb8dae0101e0b2d2b9c21103e6854544fbb66d55fc3546f4d3e69f8234257b69fa2237712af3b058a5f01ba14a340173f263b8a4bbc46dfb6f29e0584adbfffdf7a47c929d77c2d0c142afea2b05300211000000f7eeeb3f0e00000000000000000000",
|
|
||||||
"signature_hash": "",
|
|
||||||
"tx": {
|
|
||||||
"version": 2,
|
|
||||||
"unlock_time": 2852599,
|
|
||||||
"vin": [
|
|
||||||
{
|
|
||||||
"gen": {
|
|
||||||
"height": 2852539
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"vout": [
|
|
||||||
{
|
|
||||||
"amount": 601953180000,
|
|
||||||
"target": {
|
|
||||||
"tagged_key": {
|
|
||||||
"key": "e6854544fbb66d55fc3546f4d3e69f8234257b69fa2237712af3b058a5f01ba1",
|
|
||||||
"view_tag": "4a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extra": [ 1, 115, 242, 99, 184, 164, 187, 196, 109, 251, 111, 41, 224, 88, 74, 219, 255, 253, 247, 164, 124, 146, 157, 119, 194, 208, 193, 66, 175, 234, 43, 5, 48, 2, 17, 0, 0, 0, 247, 238, 235, 63, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
|
|
||||||
"rct_signatures": {
|
|
||||||
"type": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "c39652b79beb888464525fee06c3d078463af5b76d493785f8903cae93405603",
|
|
||||||
"hex": "02000102000be9aac314d8e710844d8d258133d9f701b0649e568b0cb50b1dea8103138a37c5543f3c632ef80331940cabeba29b758045db328d8d8a99de380200025155f659da61b507b0b8591cbef0ba1534b9db29a69be4a933f7897e58870b250002002de4643160fb8351a841f8079aa5af2ac62c9631bb2d5a48fcdb564cb699d12c01eaaa5acba0bc44657da783903d3de7febf7124ae03cf578938244068b475d906020901da0190b93466979e0580efcf037eafd43a457b940e592f261bd165059a9d3b92aa3baaed18346157d1361e30c553df17940985b375fc89b9c09d613d5daf007ed1ce05128cfc4d37181a5f2d6f56690357032dbcc6e85424e5ad79338801df166e5b93e427f8b86079e0de0695d24a4ac5098d98876da5f892359c982015fc0461847fd2955f6099cfc37d348bdd003f064a45c6959478807f7cb6263fc539b2616e13117fc82303aba9afeed67104c344f971a182e741436bcc040468fa6307bb478ed8ef47397e603fe7d50d3c56f5c50c4410db470e21c04b5b883a94c78bc19587d8a611c701d51b956ae06e92987ecdcd5237a65725fdcf7b52a9085831c7214b1282fbd1faf81277a6519c941907f0ef838576e610272512d1860a07a19ac9aec33168b06c937d1ead2e63e58345252be0a5fe15d38a371a6347355d16104b19f1d4a4c18e07dba72bbf8c8150bade347831b446f2ad9f5af4aa91cd35ddf3c64b3f0690c77634efd4ef07b817c6bb90a29f917769cd2978d37a14ed61b437858c93100f55b82ac7f94c0f83ac68a54df1e4d2c54b9c96661f7a41ed9fff8b28fc31949a07a7b298b5819ac7fe7c9490b20a1107ba6f6b94e6d1a58fab09bed6d096edaaea887addea1b7735228bdff409674c0f8bfceb815cdbae6fc5de41379ff842eebe205611c58d80371feb97b1f2a23fb65a474665f991113407da1c0a6f6d1fa7b6ad659f45354178270f5541eb948cbc861c53705982fa67f88c70cb936e86bf45bb17d6214ab716dc0c5fa3b7f3a181e23fad244741c1b6c44c557cf0e4dfe7a1acf75d39bfb810a798b33bcbfc73d9a11b0c648c84ed6e28b27e4aa8681aa33c0779e01ac2d16f58031d92fae578d92b6ab5d7c98a47b64ead0ad4f036922b4604d1b23dd4f4b798bae917294fc458000e7ce1449060a98c62bb1d86a0841f26e70550778103fcc3d70b9adcdb32cc750016b88178513e94d61eb65c8381532feb3368f3db60638f3b88575dded95eb92910d16c51351e7379e5df4f1fd2ffe1b80b958ef6f189ff0483a802bd1c49ed56131e1cc02dbe0949251adbc980b5a7f45317bab8be87961b6b9496d24779023f77dd1b4552fc0b522e857f38e9c63349955e3be4b3a15e86aa96943900797b8139cbfdcd940903ce41249b9e7e4a933b88a204b1ded0e2ba0fa6a1cf3ba4c3ffcc0ca96b06b400051d66ede7013ccc453e4f042e17e1091a536456460fe3551f36c99c05d59105802ee93d645167ee586e71fcc23c33d2362754065955eca45f294bb28f38960826166dfdde409f76f75366234b0a08f6e49caa1c2169e8d9376673e9835dc709bb94e4d8a164ee6278998e891e05086ad8996e496e833d975cb3d4fceb3f7203a7b365891227448b2e968b5bb653b29741bad9ba107b417354e6e3e5acf5710a346e232e9f02606a31bd8b692a67578637a5ff82339dde31425ad59ada17360184f24e6b542810ec43ef11d6a8b2eba53b288ca445d1e1a0daf059320978160eb610631a9b382291418309fc3ceecf3a08900ff5d301044a3cf565567129b10dc2aa970f43493920c4ef3df1c408e12bbcfb01573b79e296e34912cde62716033f73cf74f42b0fde655f2848bec39fd63f3485499147fe170861e9e0eae0e90d400328b595456b6451dc07eac7c8f6849dc065bb7f5ac49cff1530658bcc4d09d03e2f9611a5561dc3a90b3b2f3d933dc110db73904aeee5abcd2362a0e1b045c4cf22334e32fd33193fbfa1c4cb9e3182fb2357cf7a800f552b87579cc41d99",
|
|
||||||
"signature_hash": "686cc5232f8d0d90c6a447b10b5296c98b0b4ad5e2f88f278a6bd8f3eeb13dbf",
|
|
||||||
"tx": {
|
|
||||||
"version": 2,
|
|
||||||
"unlock_time": 0,
|
|
||||||
"vin": [
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"amount": 0,
|
|
||||||
"key_offsets": [43046249, 275416, 9860, 4749, 6529, 31705, 12848, 11038, 1547, 1461, 29],
|
|
||||||
"k_image": "ea8103138a37c5543f3c632ef80331940cabeba29b758045db328d8d8a99de38"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"vout": [
|
|
||||||
{
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"key": "5155f659da61b507b0b8591cbef0ba1534b9db29a69be4a933f7897e58870b25"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"key": "002de4643160fb8351a841f8079aa5af2ac62c9631bb2d5a48fcdb564cb699d1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extra": [ 1, 234, 170, 90, 203, 160, 188, 68, 101, 125, 167, 131, 144, 61, 61, 231, 254, 191, 113, 36, 174, 3, 207, 87, 137, 56, 36, 64, 104, 180, 117, 217, 6, 2, 9, 1, 218, 1, 144, 185, 52, 102, 151, 158],
|
|
||||||
"rct_signatures": {
|
|
||||||
"type": 5,
|
|
||||||
"txnFee": 7600000,
|
|
||||||
"ecdhInfo": [ {
|
|
||||||
"amount": "7eafd43a457b940e"
|
|
||||||
}, {
|
|
||||||
"amount": "592f261bd165059a"
|
|
||||||
}],
|
|
||||||
"outPk": [ "9d3b92aa3baaed18346157d1361e30c553df17940985b375fc89b9c09d613d5d", "af007ed1ce05128cfc4d37181a5f2d6f56690357032dbcc6e85424e5ad793388"]
|
|
||||||
},
|
|
||||||
"rctsig_prunable": {
|
|
||||||
"nbp": 1,
|
|
||||||
"bp": [ {
|
|
||||||
"A": "df166e5b93e427f8b86079e0de0695d24a4ac5098d98876da5f892359c982015",
|
|
||||||
"S": "fc0461847fd2955f6099cfc37d348bdd003f064a45c6959478807f7cb6263fc5",
|
|
||||||
"T1": "39b2616e13117fc82303aba9afeed67104c344f971a182e741436bcc040468fa",
|
|
||||||
"T2": "6307bb478ed8ef47397e603fe7d50d3c56f5c50c4410db470e21c04b5b883a94",
|
|
||||||
"taux": "c78bc19587d8a611c701d51b956ae06e92987ecdcd5237a65725fdcf7b52a908",
|
|
||||||
"mu": "5831c7214b1282fbd1faf81277a6519c941907f0ef838576e610272512d1860a",
|
|
||||||
"L": [ "a19ac9aec33168b06c937d1ead2e63e58345252be0a5fe15d38a371a6347355d", "16104b19f1d4a4c18e07dba72bbf8c8150bade347831b446f2ad9f5af4aa91cd", "35ddf3c64b3f0690c77634efd4ef07b817c6bb90a29f917769cd2978d37a14ed", "61b437858c93100f55b82ac7f94c0f83ac68a54df1e4d2c54b9c96661f7a41ed", "9fff8b28fc31949a07a7b298b5819ac7fe7c9490b20a1107ba6f6b94e6d1a58f", "ab09bed6d096edaaea887addea1b7735228bdff409674c0f8bfceb815cdbae6f", "c5de41379ff842eebe205611c58d80371feb97b1f2a23fb65a474665f9911134"],
|
|
||||||
"R": [ "da1c0a6f6d1fa7b6ad659f45354178270f5541eb948cbc861c53705982fa67f8", "8c70cb936e86bf45bb17d6214ab716dc0c5fa3b7f3a181e23fad244741c1b6c4", "4c557cf0e4dfe7a1acf75d39bfb810a798b33bcbfc73d9a11b0c648c84ed6e28", "b27e4aa8681aa33c0779e01ac2d16f58031d92fae578d92b6ab5d7c98a47b64e", "ad0ad4f036922b4604d1b23dd4f4b798bae917294fc458000e7ce1449060a98c", "62bb1d86a0841f26e70550778103fcc3d70b9adcdb32cc750016b88178513e94", "d61eb65c8381532feb3368f3db60638f3b88575dded95eb92910d16c51351e73"],
|
|
||||||
"a": "79e5df4f1fd2ffe1b80b958ef6f189ff0483a802bd1c49ed56131e1cc02dbe09",
|
|
||||||
"b": "49251adbc980b5a7f45317bab8be87961b6b9496d24779023f77dd1b4552fc0b",
|
|
||||||
"t": "522e857f38e9c63349955e3be4b3a15e86aa96943900797b8139cbfdcd940903"
|
|
||||||
}],
|
|
||||||
"CLSAGs": [ {
|
|
||||||
"s": [ "ce41249b9e7e4a933b88a204b1ded0e2ba0fa6a1cf3ba4c3ffcc0ca96b06b400", "051d66ede7013ccc453e4f042e17e1091a536456460fe3551f36c99c05d59105", "802ee93d645167ee586e71fcc23c33d2362754065955eca45f294bb28f389608", "26166dfdde409f76f75366234b0a08f6e49caa1c2169e8d9376673e9835dc709", "bb94e4d8a164ee6278998e891e05086ad8996e496e833d975cb3d4fceb3f7203", "a7b365891227448b2e968b5bb653b29741bad9ba107b417354e6e3e5acf5710a", "346e232e9f02606a31bd8b692a67578637a5ff82339dde31425ad59ada173601", "84f24e6b542810ec43ef11d6a8b2eba53b288ca445d1e1a0daf059320978160e", "b610631a9b382291418309fc3ceecf3a08900ff5d301044a3cf565567129b10d", "c2aa970f43493920c4ef3df1c408e12bbcfb01573b79e296e34912cde6271603", "3f73cf74f42b0fde655f2848bec39fd63f3485499147fe170861e9e0eae0e90d"],
|
|
||||||
"c1": "400328b595456b6451dc07eac7c8f6849dc065bb7f5ac49cff1530658bcc4d09",
|
|
||||||
"D": "d03e2f9611a5561dc3a90b3b2f3d933dc110db73904aeee5abcd2362a0e1b045"
|
|
||||||
}],
|
|
||||||
"pseudoOuts": [ "c4cf22334e32fd33193fbfa1c4cb9e3182fb2357cf7a800f552b87579cc41d99"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2f650db5bafd37ce8982f37ee443f2ecf0a8f08f639591583aecb6cd74d5a80c",
|
|
||||||
"hex": "020001020010d6f68721ea820c88d539a68f0b84af09d19401c08a02f0ee048250c219958401a49f02b33fa321a527dd227f94e759b07b2c025ce22a57db0cb062bfd1f50f6086b14ca3742730c7fa9e5d040003fcdf91296bb4560335835fda30804a7d8d200acfabe4e98a0c425d38556dac06870003d66821247fe13266bad423e445ddd6a1b51a86198e38049e2c8039ab6d5dc8b485000393ae131b8c649288a9fb61ebffa8ecb0fababa8f5159286f895f5bed10bad6388600038abfdfa2d445934fe750607f9654e02389c056644453c942d1841bbf418d94e22021014004716b1c1ffb8447e0c1d27f147a4691ed393fdf2eadb225ebfd54ffdf872f0680d5f814756596945ca3852476b456ac3c9942c978d0a3bcd9e6c236efadcdf54e6ed0db9c4bc6ac562b6859a40ad8f3bc85ca35c98badb4b4c5d43832f330d6fedb08e8f9e2acd339c648bf03957cb02aa69b8ab15326e3bbe1ce35df677306edabd89e5635f226a743068500e25028fbdbf1ea19d0921a27c8baec842b753080f407ee4b9a87f2c525e9bfb61fb4d14187c0577e799bf20e53a86359cb75f40ee4d291017c2b59e7278c94b6296dee9ac65ed5ccf61a77ba4f1b3edfb13c5d02748763b23a6bac2f6a891b474d55b625030b35f9b7b564e747afd4cb8e1ce830a9bc59fd6e146443965494f94a8433de054080fcb71f8d48803598cc91db3c7b3fd190ea8ff5f67980a63de4cb9cd06568a9b27aa994992bc33d70990225acb09faf68066aa27c1118c685cb8f3516c3b664450fabdced384de01650d6455287bc0f210aaa5c173c491844155736a64d7cbdda79f0c8a5ccc07d187ca112664a0e6eb500087178983179f1ba2ffb030d577638001b58f5e621b4723e5b0bd0853fb430113d03efd026660a18a23c7582e9788f770212b604759aa242b35b3ca4a835bb18881c8593ac4247ba51ea95946cf079721588bac494f563a687fe1010818caa94583969b0f4a4a40eeee395cbb7881a53d98cad51b1e5d12c7071a7424b4c534e32c53a31b11e6151edd0a13ef9695021bff9bd4c62df9a62d9e0fbd01e750d0b6abc56cc96d55ef06f6428b42fc63f6610633ecf023211e64a1ff89dcabfeeb4b938e64312dcc849929e8d4a290eff601e06dc65141665d7b312ac1f0f859a00fd6d6ccf7dc695e7ae3cf44bed1d9c8659ee3451dd3498f462912ba881a473c9bc0866e4fb33114b2ef7c25869f9cc3c40a06fc2407e2c678126ff1c38a35c5c949bc219fb33ba15730510c41554c727d5adfce33a518148234e0aa5411cb20c115e749792ad47ee19e9f1544dba61593d95cb98d4720a8ae6e60146416d673e5707c3de31d91043422ab848d4676a6845ced6e7075c5a09bc8b4e0ad706c8c07bda527a7325771438e04f37517f3ca5262ef2ddfc9e13db988a90c50be5422a83ad75b93f4faae980d6e6a3abfd0e96387121101afaf55f425dc876d9a8735c1e29d823e19fee5e502c18d16ec9225f232cfbc3dcd143aaa1904f42e880b612beeea3e5a745a7f32e6b2135a75f71117e2947c99647f14702417a9a76f6130b5d62fd149a606061709a86253c3c2a30c8ccc0e2b5ee636bda81973b011fa8b96e0f9149e7d02d903e982b025e0944029423ba9318637387d6f0a8a75f1fa957950ce6661368738251a418968ae390143e596a77bef7de4008ca66ed28b82e044d0ab293f792e8b1e9c1bc24b14ee53539f535b05f2f336c1b7698ca3cb1dc8a3a09568c6841724a19d412d4313760e3560616df7f5b2250b1a52bf32922b3964309b0bedb645579ee09d87959f4e997e4792ac9fa26858ef1aa1dbf7b10da08e7092cb200369d75f3d2b81ad2c237954cdfea1d173f84122ce4cf82a9ebaa04650a69f3675f2155bbb7ce508fdd6a328492b8788e37809f2accf082387b97a7660d427cac9eb93ceacda0cdb9db95a2d6c6fa9ca86276acce2cb8e432b14efb4d0e8a1f3cbc8534c5dfb9a42f7b0d5c212928115cb2c5b905c650b5325e2a849109c60329dcc20f1c1f10d9f6a87d17359938c520e00dd3f5e1857b5af502cc590cad89abca61f4a94513d8e42db9e7223b5d97afd80f490155bf49b79c7ea5c10d6cb74ba10211d6ec75458436a08794164d16bcb4d092274061449418d9fc3d0a9947a8854a399c7e77a49568676ff8df07c3aa21ca90a611dcdfe0c6bd44690a43a3263237f1def6658ba936e2f17c3853fdcd2c0e24cc0b26c59abb47031e00992ca59657da958b48d21d12ae0a93a68596b72c6cc826fd8e079de67b0539026a24c5dcea4875f16cd0722352424493647f7ad3b3148bcdf6c8504c25bbbb07a8b01a6352cb602a1964c02e7e10601644cee41c2bdbb39a9687fdd78dca919726312d076b9e7a4e5b0324e305b99bb1c3ea40bd2296de41f2fc43f668e1a9fb",
|
|
||||||
"signature_hash": "9c13c702e03b54a3000a008e4deb1763d7e232c3378bf928df1e2e976f5ba9c5",
|
|
||||||
"tx": {
|
|
||||||
"version": 2,
|
|
||||||
"unlock_time": 0,
|
|
||||||
"vin": [ {
|
|
||||||
"key": {
|
|
||||||
"amount": 0,
|
|
||||||
"key_offsets": [69335894, 196970, 944776, 182182, 153476, 19025, 34112, 79728, 10242, 3266, 16917, 36772, 8115, 4259, 5029, 4445],
|
|
||||||
"k_image": "7f94e759b07b2c025ce22a57db0cb062bfd1f50f6086b14ca3742730c7fa9e5d"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"vout": [
|
|
||||||
{
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"tagged_key": {
|
|
||||||
"key": "fcdf91296bb4560335835fda30804a7d8d200acfabe4e98a0c425d38556dac06",
|
|
||||||
"view_tag": "87"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"tagged_key": {
|
|
||||||
"key": "d66821247fe13266bad423e445ddd6a1b51a86198e38049e2c8039ab6d5dc8b4",
|
|
||||||
"view_tag": "85"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"tagged_key": {
|
|
||||||
"key": "93ae131b8c649288a9fb61ebffa8ecb0fababa8f5159286f895f5bed10bad638",
|
|
||||||
"view_tag": "86"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"tagged_key": {
|
|
||||||
"key": "8abfdfa2d445934fe750607f9654e02389c056644453c942d1841bbf418d94e2",
|
|
||||||
"view_tag": "20"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extra": [ 1, 64, 4, 113, 107, 28, 31, 251, 132, 71, 224, 193, 210, 127, 20, 122, 70, 145, 237, 57, 63, 223, 46, 173, 178, 37, 235, 253, 84, 255, 223, 135, 47],
|
|
||||||
"rct_signatures": {
|
|
||||||
"type": 6,
|
|
||||||
"txnFee": 43920000,
|
|
||||||
"ecdhInfo": [ {
|
|
||||||
"amount": "756596945ca38524"
|
|
||||||
}, {
|
|
||||||
"amount": "76b456ac3c9942c9"
|
|
||||||
}, {
|
|
||||||
"amount": "78d0a3bcd9e6c236"
|
|
||||||
}, {
|
|
||||||
"amount": "efadcdf54e6ed0db"
|
|
||||||
}],
|
|
||||||
"outPk": [ "9c4bc6ac562b6859a40ad8f3bc85ca35c98badb4b4c5d43832f330d6fedb08e8", "f9e2acd339c648bf03957cb02aa69b8ab15326e3bbe1ce35df677306edabd89e", "5635f226a743068500e25028fbdbf1ea19d0921a27c8baec842b753080f407ee", "4b9a87f2c525e9bfb61fb4d14187c0577e799bf20e53a86359cb75f40ee4d291"]
|
|
||||||
},
|
|
||||||
"rctsig_prunable": {
|
|
||||||
"nbp": 1,
|
|
||||||
"bpp": [ {
|
|
||||||
"A": "7c2b59e7278c94b6296dee9ac65ed5ccf61a77ba4f1b3edfb13c5d02748763b2",
|
|
||||||
"A1": "3a6bac2f6a891b474d55b625030b35f9b7b564e747afd4cb8e1ce830a9bc59fd",
|
|
||||||
"B": "6e146443965494f94a8433de054080fcb71f8d48803598cc91db3c7b3fd190ea",
|
|
||||||
"r1": "8ff5f67980a63de4cb9cd06568a9b27aa994992bc33d70990225acb09faf6806",
|
|
||||||
"s1": "6aa27c1118c685cb8f3516c3b664450fabdced384de01650d6455287bc0f210a",
|
|
||||||
"d1": "aa5c173c491844155736a64d7cbdda79f0c8a5ccc07d187ca112664a0e6eb500",
|
|
||||||
"L": [ "7178983179f1ba2ffb030d577638001b58f5e621b4723e5b0bd0853fb430113d", "03efd026660a18a23c7582e9788f770212b604759aa242b35b3ca4a835bb1888", "1c8593ac4247ba51ea95946cf079721588bac494f563a687fe1010818caa9458", "3969b0f4a4a40eeee395cbb7881a53d98cad51b1e5d12c7071a7424b4c534e32", "c53a31b11e6151edd0a13ef9695021bff9bd4c62df9a62d9e0fbd01e750d0b6a", "bc56cc96d55ef06f6428b42fc63f6610633ecf023211e64a1ff89dcabfeeb4b9", "38e64312dcc849929e8d4a290eff601e06dc65141665d7b312ac1f0f859a00fd", "6d6ccf7dc695e7ae3cf44bed1d9c8659ee3451dd3498f462912ba881a473c9bc"
|
|
||||||
],
|
|
||||||
"R": [ "66e4fb33114b2ef7c25869f9cc3c40a06fc2407e2c678126ff1c38a35c5c949b", "c219fb33ba15730510c41554c727d5adfce33a518148234e0aa5411cb20c115e", "749792ad47ee19e9f1544dba61593d95cb98d4720a8ae6e60146416d673e5707", "c3de31d91043422ab848d4676a6845ced6e7075c5a09bc8b4e0ad706c8c07bda", "527a7325771438e04f37517f3ca5262ef2ddfc9e13db988a90c50be5422a83ad", "75b93f4faae980d6e6a3abfd0e96387121101afaf55f425dc876d9a8735c1e29", "d823e19fee5e502c18d16ec9225f232cfbc3dcd143aaa1904f42e880b612beee", "a3e5a745a7f32e6b2135a75f71117e2947c99647f14702417a9a76f6130b5d62"
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
"CLSAGs": [ {
|
|
||||||
"s": [ "fd149a606061709a86253c3c2a30c8ccc0e2b5ee636bda81973b011fa8b96e0f", "9149e7d02d903e982b025e0944029423ba9318637387d6f0a8a75f1fa957950c", "e6661368738251a418968ae390143e596a77bef7de4008ca66ed28b82e044d0a", "b293f792e8b1e9c1bc24b14ee53539f535b05f2f336c1b7698ca3cb1dc8a3a09", "568c6841724a19d412d4313760e3560616df7f5b2250b1a52bf32922b3964309", "b0bedb645579ee09d87959f4e997e4792ac9fa26858ef1aa1dbf7b10da08e709", "2cb200369d75f3d2b81ad2c237954cdfea1d173f84122ce4cf82a9ebaa04650a", "69f3675f2155bbb7ce508fdd6a328492b8788e37809f2accf082387b97a7660d", "427cac9eb93ceacda0cdb9db95a2d6c6fa9ca86276acce2cb8e432b14efb4d0e", "8a1f3cbc8534c5dfb9a42f7b0d5c212928115cb2c5b905c650b5325e2a849109", "c60329dcc20f1c1f10d9f6a87d17359938c520e00dd3f5e1857b5af502cc590c", "ad89abca61f4a94513d8e42db9e7223b5d97afd80f490155bf49b79c7ea5c10d", "6cb74ba10211d6ec75458436a08794164d16bcb4d092274061449418d9fc3d0a", "9947a8854a399c7e77a49568676ff8df07c3aa21ca90a611dcdfe0c6bd44690a", "43a3263237f1def6658ba936e2f17c3853fdcd2c0e24cc0b26c59abb47031e00", "992ca59657da958b48d21d12ae0a93a68596b72c6cc826fd8e079de67b053902"],
|
|
||||||
"c1": "6a24c5dcea4875f16cd0722352424493647f7ad3b3148bcdf6c8504c25bbbb07",
|
|
||||||
"D": "a8b01a6352cb602a1964c02e7e10601644cee41c2bdbb39a9687fdd78dca9197"
|
|
||||||
}],
|
|
||||||
"pseudoOuts": [ "26312d076b9e7a4e5b0324e305b99bb1c3ea40bd2296de41f2fc43f668e1a9fb"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "f66f36be5a6b340bc8515d3606d4beceb20611dddb1802b387fbaba30c5c98d3",
|
|
||||||
"hex": "02000102000bf59ea50bf48bfb08e1d6a1039843f7ee0597d002ba3ca603de3be263ca194830cafb5a73ad93cd2fe5271505596a75d7cabb01ced2bb608028245ea73bb8020002fc3f396be673a4957fbc1976601941d225ffdbec54bc06461698d14fda7c8b1f00022757dd54027e93c917251de2cc6777f7a3fa484f5b244ab54bf8783e7da80c362c01959377b2cc5b76f40886262064cc71324414c2996720dcbea25eae8b8faf4f9802090126a37bb1d1414ab705c0cef71cf0704cd0b1fcde1d656737377f5106deb167d7cde7206c17c8f2f25a508be29e1ad78bb792c3fedcbdd9ce95815c59a5fd98ff5b251f105f3d51067fb90cb9f1e0b6138dfda82aad4906472cf7f8c1b501c786dc1c545d39b00502134e4a2935b9b81f420f4d926bed61b2dde30fde4a464d85cbfda1df5a07da2d4d135ff618e5cf4d6b22238b913af712dd59cb228fec35fc0639d3b54edf518e507034b5be35523ac3c98396bd9cc6e6a59e1c6eda6e93f9e84c4fdade80a6f449ac6ccf8d6583fcd495b78c53a43321210d73370f86999fcc79761f1810514b50b8a1fee1288289b64718d54bcef42abfe61fb8fb0f60373d190d80ad65cabd36d9600d6253f8d343961367526122ca0c5b0acdc60c071f5ab47025d4d89568a2f0b56fc73c6488cc4500e398d2b3059e0d35cede35c33b473e6e57121629db88f0f8c15e03036eae2887f1d9d76d90a0b5a5caa01272347c96e88e461acb04a6be5624c4a6742dd8a0d36ac75e7056ffb3a3a7ae68dda87894c7f503a794dc4458ff058e6f7cd903662e5961d5eeb052b8f075a5ad5cb84407f96ce47a5793ad0c8e4060ee4d90c9946c54b83e91737ccf71acc00045a919866586941d8deeb467a7d83335f84a10d66ba1f51afdf961649ac95ad97dd24d553c1c1004a73332d225d29d0c62bee22e2ea81ceffb02f27ff05a12144a076ed84bce66dd6e84d56fedf06c180a504706977084ff0a74174da623fcb03a88b0246b5a76de445334f447c4525e524e0d3e6b8751417249e0eb6f498ba12ccee562dfddb048ddbd5131263e210d54f04ec38a41c64f7a5812c2083b9fc46fecc2de7f0d1d0aeaaa2f2a9d956879f66e563b48ff9476d67ab98f6956019bf0a36428b0361b28383e7ef2b90bad6a66ae286af4753e54fe6be3131fdc9986ccdf04cbc167abdfd181b326a1d8b30990b9df0b1e702c7bab83c199aa1341c6b4ae1e227b831f0d068e619bb73ed5de32b0bfda62c5203be7f2a6a37c3feb4663da413387e0255ec0204e469fed5e0fde75662cf3a90ae0044778a5a75a319dec07d2d84c70c9b51c9a31f9bc7f25f1a89033dbd23072ac38eb59e08d6ad4f321011a08df0f0559d3b6dd8a22042b40be532d2fc811e42774cd129dc9c9e671600c3a7fe60336836a1e71f2bfeee69d2ae5e647b54dc1c54e9993a64c0f42daf70aaab2387b9a0fdcc82a7e2f3b52ed2a8135b55f166cb49fb6b1d34a64d30f370d8271408d0e4db75616b758014671d321c8c5086a8d7b1bedd44bcb75b382c60bffb7c7726232426ea19c9bd622ee096e772a4c5ab6305a6b2f27fb4a5f60e40737a4cc043ea061755ace64393a0af82ea8088307426acb33a34de95ea7252c01a5b07847f707777b8cd64cc73364a8e65181227ba1ba5aa63161408a7265980b6b6c18079d195a12ec7af4404ff61d3c756aa35b88e4fe4bd72c8b22298b1601b1d04f9861e7118f10808505812809d54d85aa79f4ceb905c8e87b1a5801c60bb12b0f3fdc0e5e0afb7839fd51742aa88ef4466bc1e1f9c1b6f978f736b1880c9a902621eaca740aa21cfaf36931e09b7cc3b28223ad6c2398bd828c7270460d78dd6dd5d9c181aed832e62d56b00e870961b0b6a3a77eb4604cef64f69898046ba7157673909c3b2cb3bedef665e83c364475d482dfa46e717c4c5fa29b7f09dda97110c8a66b48e8fab4cbff2578e4cb85e6b353bfe8f78b6f9182711de13c7093d2007f1dbdfe5f46332b797376af80efc67ab028c597d528461384683dae",
|
|
||||||
"signature_hash": "8cb405e1460df8134032db1430e1cfffb8f707c9de43ba1f68100f2af8a5e6b1",
|
|
||||||
"tx": {
|
|
||||||
"version": 2,
|
|
||||||
"unlock_time": 0,
|
|
||||||
"vin": [ {
|
|
||||||
"key": {
|
|
||||||
"amount": 0,
|
|
||||||
"key_offsets": [ 23678837, 18793972, 6843233, 8600, 96119, 43031, 7738, 422, 7646, 12770, 3274],
|
|
||||||
"k_image": "4830cafb5a73ad93cd2fe5271505596a75d7cabb01ced2bb608028245ea73bb8"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"vout": [
|
|
||||||
{
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"key": "fc3f396be673a4957fbc1976601941d225ffdbec54bc06461698d14fda7c8b1f"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 0,
|
|
||||||
"target": {
|
|
||||||
"key": "2757dd54027e93c917251de2cc6777f7a3fa484f5b244ab54bf8783e7da80c36"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extra": [ 1, 149, 147, 119, 178, 204, 91, 118, 244, 8, 134, 38, 32, 100, 204, 113, 50, 68, 20, 194, 153, 103, 32, 220, 190, 162, 94, 174, 139, 143, 175, 79, 152, 2, 9, 1, 38, 163, 123, 177, 209, 65, 74, 183],
|
|
||||||
"rct_signatures": {
|
|
||||||
"type": 5,
|
|
||||||
"txnFee": 60680000,
|
|
||||||
"ecdhInfo": [ {
|
|
||||||
"amount": "f0704cd0b1fcde1d"
|
|
||||||
}, {
|
|
||||||
"amount": "656737377f5106de"
|
|
||||||
}],
|
|
||||||
"outPk": [ "b167d7cde7206c17c8f2f25a508be29e1ad78bb792c3fedcbdd9ce95815c59a5", "fd98ff5b251f105f3d51067fb90cb9f1e0b6138dfda82aad4906472cf7f8c1b5"]
|
|
||||||
},
|
|
||||||
"rctsig_prunable": {
|
|
||||||
"nbp": 1,
|
|
||||||
"bp": [ {
|
|
||||||
"A": "c786dc1c545d39b00502134e4a2935b9b81f420f4d926bed61b2dde30fde4a46",
|
|
||||||
"S": "4d85cbfda1df5a07da2d4d135ff618e5cf4d6b22238b913af712dd59cb228fec",
|
|
||||||
"T1": "35fc0639d3b54edf518e507034b5be35523ac3c98396bd9cc6e6a59e1c6eda6e",
|
|
||||||
"T2": "93f9e84c4fdade80a6f449ac6ccf8d6583fcd495b78c53a43321210d73370f86",
|
|
||||||
"taux": "999fcc79761f1810514b50b8a1fee1288289b64718d54bcef42abfe61fb8fb0f",
|
|
||||||
"mu": "60373d190d80ad65cabd36d9600d6253f8d343961367526122ca0c5b0acdc60c",
|
|
||||||
"L": [ "1f5ab47025d4d89568a2f0b56fc73c6488cc4500e398d2b3059e0d35cede35c3", "3b473e6e57121629db88f0f8c15e03036eae2887f1d9d76d90a0b5a5caa01272", "347c96e88e461acb04a6be5624c4a6742dd8a0d36ac75e7056ffb3a3a7ae68dd", "a87894c7f503a794dc4458ff058e6f7cd903662e5961d5eeb052b8f075a5ad5c", "b84407f96ce47a5793ad0c8e4060ee4d90c9946c54b83e91737ccf71acc00045", "a919866586941d8deeb467a7d83335f84a10d66ba1f51afdf961649ac95ad97d", "d24d553c1c1004a73332d225d29d0c62bee22e2ea81ceffb02f27ff05a12144a"
|
|
||||||
],
|
|
||||||
"R": [ "6ed84bce66dd6e84d56fedf06c180a504706977084ff0a74174da623fcb03a88", "b0246b5a76de445334f447c4525e524e0d3e6b8751417249e0eb6f498ba12cce", "e562dfddb048ddbd5131263e210d54f04ec38a41c64f7a5812c2083b9fc46fec", "c2de7f0d1d0aeaaa2f2a9d956879f66e563b48ff9476d67ab98f6956019bf0a3", "6428b0361b28383e7ef2b90bad6a66ae286af4753e54fe6be3131fdc9986ccdf", "04cbc167abdfd181b326a1d8b30990b9df0b1e702c7bab83c199aa1341c6b4ae", "1e227b831f0d068e619bb73ed5de32b0bfda62c5203be7f2a6a37c3feb4663da"
|
|
||||||
],
|
|
||||||
"a": "413387e0255ec0204e469fed5e0fde75662cf3a90ae0044778a5a75a319dec07",
|
|
||||||
"b": "d2d84c70c9b51c9a31f9bc7f25f1a89033dbd23072ac38eb59e08d6ad4f32101",
|
|
||||||
"t": "1a08df0f0559d3b6dd8a22042b40be532d2fc811e42774cd129dc9c9e671600c"
|
|
||||||
}],
|
|
||||||
"CLSAGs": [ {
|
|
||||||
"s": [ "3a7fe60336836a1e71f2bfeee69d2ae5e647b54dc1c54e9993a64c0f42daf70a", "aab2387b9a0fdcc82a7e2f3b52ed2a8135b55f166cb49fb6b1d34a64d30f370d", "8271408d0e4db75616b758014671d321c8c5086a8d7b1bedd44bcb75b382c60b", "ffb7c7726232426ea19c9bd622ee096e772a4c5ab6305a6b2f27fb4a5f60e407", "37a4cc043ea061755ace64393a0af82ea8088307426acb33a34de95ea7252c01", "a5b07847f707777b8cd64cc73364a8e65181227ba1ba5aa63161408a7265980b", "6b6c18079d195a12ec7af4404ff61d3c756aa35b88e4fe4bd72c8b22298b1601", "b1d04f9861e7118f10808505812809d54d85aa79f4ceb905c8e87b1a5801c60b", "b12b0f3fdc0e5e0afb7839fd51742aa88ef4466bc1e1f9c1b6f978f736b1880c", "9a902621eaca740aa21cfaf36931e09b7cc3b28223ad6c2398bd828c7270460d", "78dd6dd5d9c181aed832e62d56b00e870961b0b6a3a77eb4604cef64f6989804"],
|
|
||||||
"c1": "6ba7157673909c3b2cb3bedef665e83c364475d482dfa46e717c4c5fa29b7f09",
|
|
||||||
"D": "dda97110c8a66b48e8fab4cbff2578e4cb85e6b353bfe8f78b6f9182711de13c"
|
|
||||||
}],
|
|
||||||
"pseudoOuts": [ "7093d2007f1dbdfe5f46332b797376af80efc67ab028c597d528461384683dae"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "55ba10662968c57fc8fed2c82a99d6fd9516730c245f58e9e87bb9a35378014a",
|
|
||||||
"hex": "01000302b0f9cf0e0100e53d3d97d11974ccf49d23513b9465bc139bda14b8207288e41557707e59c2dc02c08092de06010133e69f524f1989738827c9fb9087d45d7b6865645453f620189939d926735b3902b0b2c4f62201001d680e360c156c7cc952b0c4fc39a39c95a766423109766ae3dcf0c5cf8abf9e06a0f736026bcac41f5468fd1bcf8994e1a4282aecb420005ec294324faec5c6f46806614780c8afa025027998ef0ac319d96b224fa3c33fe12677ef06f4aed80916f28e3f7a684a1c89d78094ebdc03028e0004b98f7c622f1d0364a2d0270c40e6606793bd948a41af9287016d264d7580ade20402f911ad66eabcd8112e90130d7594cb8fc413431da9b5b004c8651d100a2ac8fb8084af5f029caa24c41b1b4938e7e066a7e59592fdc3c832e2516048e57af5d8acca9bef0ac0b80202e99b6a2e000f03f73c1966e16875945ffad20b1895efa17202c6e240191b01e421011da6ff966df43bd44f513aaafa260c54e2ad31469664fa7b0a44fed4ead9a483056cd88c350b340289694ad525f1316367ed16673dc23911e624e6bac4a48b032a623d09ce5bd85a839534de4fbbfb72da2a6779a66f775c16379e8abd122401a5a19c47bb8ecd0c7aa9610513b611602d246f3b07fa19512dc8fde7b180c704694b2d75abbaa2907cda52f888f18ded34308dd1b40f1fb33a01340a1ddc5f07ce19ab4357c52764600861d2d331cfed5972fff42dac64583d617fa4ee27a509a8c29ff77fc71bc7cd106ce54cd3020d1c18c6c794aaa93cb32cd4ff55d82b04",
|
|
||||||
"signature_hash": "1ad261b4c8f35b8861c4f3a78b240a85e44be6a8ac49acd1e50de4680adf7fac",
|
|
||||||
"tx": {
|
|
||||||
"version": 1,
|
|
||||||
"unlock_time": 0,
|
|
||||||
"vin": [
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"amount": 30670000,
|
|
||||||
"key_offsets": [0],
|
|
||||||
"k_image": "e53d3d97d11974ccf49d23513b9465bc139bda14b8207288e41557707e59c2dc"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"amount": 1808040000,
|
|
||||||
"key_offsets": [1],
|
|
||||||
"k_image": "33e69f524f1989738827c9fb9087d45d7b6865645453f620189939d926735b39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"amount": 9375390000,
|
|
||||||
"key_offsets": [0],
|
|
||||||
"k_image": "1d680e360c156c7cc952b0c4fc39a39c95a766423109766ae3dcf0c5cf8abf9e"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"vout": [
|
|
||||||
{
|
|
||||||
"amount": 900000,
|
|
||||||
"target": {
|
|
||||||
"key": "6bcac41f5468fd1bcf8994e1a4282aecb420005ec294324faec5c6f468066147"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 10000000000,
|
|
||||||
"target": {
|
|
||||||
"key": "7998ef0ac319d96b224fa3c33fe12677ef06f4aed80916f28e3f7a684a1c89d7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 1000000000,
|
|
||||||
"target": {
|
|
||||||
"key": "8e0004b98f7c622f1d0364a2d0270c40e6606793bd948a41af9287016d264d75"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 10000000,
|
|
||||||
"target": {
|
|
||||||
"key": "f911ad66eabcd8112e90130d7594cb8fc413431da9b5b004c8651d100a2ac8fb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 200000000,
|
|
||||||
"target": {
|
|
||||||
"key": "9caa24c41b1b4938e7e066a7e59592fdc3c832e2516048e57af5d8acca9bef0a"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 40000,
|
|
||||||
"target": {
|
|
||||||
"key": "e99b6a2e000f03f73c1966e16875945ffad20b1895efa17202c6e240191b01e4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extra": [ 1, 29, 166, 255, 150, 109, 244, 59, 212, 79, 81, 58, 170, 250, 38, 12, 84, 226, 173, 49, 70, 150, 100, 250, 123, 10, 68, 254, 212, 234, 217, 164, 131],
|
|
||||||
"signatures": [ "056cd88c350b340289694ad525f1316367ed16673dc23911e624e6bac4a48b032a623d09ce5bd85a839534de4fbbfb72da2a6779a66f775c16379e8abd122401", "a5a19c47bb8ecd0c7aa9610513b611602d246f3b07fa19512dc8fde7b180c704694b2d75abbaa2907cda52f888f18ded34308dd1b40f1fb33a01340a1ddc5f07", "ce19ab4357c52764600861d2d331cfed5972fff42dac64583d617fa4ee27a509a8c29ff77fc71bc7cd106ce54cd3020d1c18c6c794aaa93cb32cd4ff55d82b04"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,635 +0,0 @@
|
|||||||
use core::cmp::Ordering;
|
|
||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
io::*,
|
|
||||||
primitives::keccak256,
|
|
||||||
ring_signatures::RingSignature,
|
|
||||||
ringct::{bulletproofs::Bulletproof, PrunedRctProofs},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An input in the Monero protocol.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum Input {
|
|
||||||
/// An input for a miner transaction, which is generating new coins.
|
|
||||||
Gen(usize),
|
|
||||||
/// An input spending an output on-chain.
|
|
||||||
ToKey {
|
|
||||||
/// The pool this input spends an output of.
|
|
||||||
amount: Option<u64>,
|
|
||||||
/// The decoys used by this input's ring, specified as their offset distance from each other.
|
|
||||||
key_offsets: Vec<u64>,
|
|
||||||
/// The key image (linking tag, nullifer) for the spent output.
|
|
||||||
key_image: EdwardsPoint,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Input {
|
|
||||||
/// Write the Input.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Input::Gen(height) => {
|
|
||||||
w.write_all(&[255])?;
|
|
||||||
write_varint(height, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
Input::ToKey { amount, key_offsets, key_image } => {
|
|
||||||
w.write_all(&[2])?;
|
|
||||||
write_varint(&amount.unwrap_or(0), w)?;
|
|
||||||
write_vec(write_varint, key_offsets, w)?;
|
|
||||||
write_point(key_image, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the Input to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = vec![];
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an Input.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
|
|
||||||
Ok(match read_byte(r)? {
|
|
||||||
255 => Input::Gen(read_varint(r)?),
|
|
||||||
2 => {
|
|
||||||
let amount = read_varint(r)?;
|
|
||||||
// https://github.com/monero-project/monero/
|
|
||||||
// blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
|
|
||||||
// src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863
|
|
||||||
// A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output
|
|
||||||
// That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT
|
|
||||||
let amount = if amount == 0 { None } else { Some(amount) };
|
|
||||||
Input::ToKey {
|
|
||||||
amount,
|
|
||||||
key_offsets: read_vec(read_varint, None, r)?,
|
|
||||||
key_image: read_torsion_free_point(r)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An output in the Monero protocol.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Output {
|
|
||||||
/// The pool this output should be sorted into.
|
|
||||||
pub amount: Option<u64>,
|
|
||||||
/// The key which can spend this output.
|
|
||||||
pub key: CompressedEdwardsY,
|
|
||||||
/// The view tag for this output, as used to accelerate scanning.
|
|
||||||
pub view_tag: Option<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Output {
|
|
||||||
/// Write the Output.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
write_varint(&self.amount.unwrap_or(0), w)?;
|
|
||||||
w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
|
|
||||||
w.write_all(&self.key.to_bytes())?;
|
|
||||||
if let Some(view_tag) = self.view_tag {
|
|
||||||
w.write_all(&[view_tag])?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Output to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(8 + 1 + 32);
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an Output.
|
|
||||||
pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> {
|
|
||||||
let amount = read_varint(r)?;
|
|
||||||
let amount = if rct {
|
|
||||||
if amount != 0 {
|
|
||||||
Err(io::Error::other("RCT TX output wasn't 0"))?;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(amount)
|
|
||||||
};
|
|
||||||
|
|
||||||
let view_tag = match read_byte(r)? {
|
|
||||||
2 => false,
|
|
||||||
3 => true,
|
|
||||||
_ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Output {
|
|
||||||
amount,
|
|
||||||
key: CompressedEdwardsY(read_bytes(r)?),
|
|
||||||
view_tag: if view_tag { Some(read_byte(r)?) } else { None },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An additional timelock for a Monero transaction.
|
|
||||||
///
|
|
||||||
/// Monero outputs are locked by a default timelock. If a timelock is explicitly specified, the
|
|
||||||
/// longer of the two will be the timelock used.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub enum Timelock {
|
|
||||||
/// No additional timelock.
|
|
||||||
None,
|
|
||||||
/// Additionally locked until this block.
|
|
||||||
Block(usize),
|
|
||||||
/// Additionally locked until this many seconds since the epoch.
|
|
||||||
Time(u64),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Timelock {
|
|
||||||
/// Write the Timelock.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Timelock::None => write_varint(&0u8, w),
|
|
||||||
Timelock::Block(block) => write_varint(block, w),
|
|
||||||
Timelock::Time(time) => write_varint(time, w),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the Timelock to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(1);
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Timelock.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
const TIMELOCK_BLOCK_THRESHOLD: usize = 500_000_000;
|
|
||||||
|
|
||||||
let raw = read_varint::<_, u64>(r)?;
|
|
||||||
Ok(if raw == 0 {
|
|
||||||
Timelock::None
|
|
||||||
} else if raw <
|
|
||||||
u64::try_from(TIMELOCK_BLOCK_THRESHOLD)
|
|
||||||
.expect("TIMELOCK_BLOCK_THRESHOLD didn't fit in a u64")
|
|
||||||
{
|
|
||||||
Timelock::Block(usize::try_from(raw).expect(
|
|
||||||
"timelock overflowed usize despite being less than a const representable with a usize",
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Timelock::Time(raw)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for Timelock {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
match (self, other) {
|
|
||||||
(Timelock::None, Timelock::None) => Some(Ordering::Equal),
|
|
||||||
(Timelock::None, _) => Some(Ordering::Less),
|
|
||||||
(_, Timelock::None) => Some(Ordering::Greater),
|
|
||||||
(Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b),
|
|
||||||
(Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The transaction prefix.
|
|
||||||
///
|
|
||||||
/// This is common to all transaction versions and contains most parts of the transaction needed to
|
|
||||||
/// handle it. It excludes any proofs.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct TransactionPrefix {
|
|
||||||
/// The timelock this transaction is additionally constrained by.
|
|
||||||
///
|
|
||||||
/// All transactions on the blockchain are subject to a 10-block lock. This adds a further
|
|
||||||
/// constraint.
|
|
||||||
pub additional_timelock: Timelock,
|
|
||||||
/// The inputs for this transaction.
|
|
||||||
pub inputs: Vec<Input>,
|
|
||||||
/// The outputs for this transaction.
|
|
||||||
pub outputs: Vec<Output>,
|
|
||||||
/// The additional data included within the transaction.
|
|
||||||
///
|
|
||||||
/// This is an arbitrary data field, yet is used by wallets for containing the data necessary to
|
|
||||||
/// scan the transaction.
|
|
||||||
pub extra: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransactionPrefix {
|
|
||||||
/// Write a TransactionPrefix.
|
|
||||||
///
|
|
||||||
/// This is distinct from Monero in that it won't write any version.
|
|
||||||
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.additional_timelock.write(w)?;
|
|
||||||
write_vec(Input::write, &self.inputs, w)?;
|
|
||||||
write_vec(Output::write, &self.outputs, w)?;
|
|
||||||
write_varint(&self.extra.len(), w)?;
|
|
||||||
w.write_all(&self.extra)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a TransactionPrefix.
|
|
||||||
///
|
|
||||||
/// This is distinct from Monero in that it won't read the version. The version must be passed
|
|
||||||
/// in.
|
|
||||||
pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
|
|
||||||
let additional_timelock = Timelock::read(r)?;
|
|
||||||
|
|
||||||
let inputs = read_vec(|r| Input::read(r), None, r)?;
|
|
||||||
if inputs.is_empty() {
|
|
||||||
Err(io::Error::other("transaction had no inputs"))?;
|
|
||||||
}
|
|
||||||
let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
|
|
||||||
|
|
||||||
let mut prefix = TransactionPrefix {
|
|
||||||
additional_timelock,
|
|
||||||
inputs,
|
|
||||||
outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), None, r)?,
|
|
||||||
extra: vec![],
|
|
||||||
};
|
|
||||||
prefix.extra = read_vec(read_byte, None, r)?;
|
|
||||||
Ok(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash(&self, version: u64) -> [u8; 32] {
|
|
||||||
let mut buf = vec![];
|
|
||||||
write_varint(&version, &mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
keccak256(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod sealed {
|
|
||||||
use core::fmt::Debug;
|
|
||||||
use crate::ringct::*;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub(crate) trait RingSignatures: Clone + PartialEq + Eq + Default + Debug {
|
|
||||||
fn signatures_to_write(&self) -> &[RingSignature];
|
|
||||||
fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RingSignatures for Vec<RingSignature> {
|
|
||||||
fn signatures_to_write(&self) -> &[RingSignature] {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self> {
|
|
||||||
let mut signatures = Vec::with_capacity(inputs.len());
|
|
||||||
for input in inputs {
|
|
||||||
match input {
|
|
||||||
Input::ToKey { key_offsets, .. } => {
|
|
||||||
signatures.push(RingSignature::read(key_offsets.len(), r)?)
|
|
||||||
}
|
|
||||||
_ => Err(io::Error::other("reading signatures for a transaction with non-ToKey inputs"))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(signatures)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RingSignatures for () {
|
|
||||||
fn signatures_to_write(&self) -> &[RingSignature] {
|
|
||||||
&[]
|
|
||||||
}
|
|
||||||
fn read_signatures(_: &[Input], _: &mut impl Read) -> io::Result<Self> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait RctProofsTrait: Clone + PartialEq + Eq + Debug {
|
|
||||||
fn write(&self, w: &mut impl Write) -> io::Result<()>;
|
|
||||||
fn read(
|
|
||||||
ring_length: usize,
|
|
||||||
inputs: usize,
|
|
||||||
outputs: usize,
|
|
||||||
r: &mut impl Read,
|
|
||||||
) -> io::Result<Option<Self>>;
|
|
||||||
fn rct_type(&self) -> RctType;
|
|
||||||
fn base(&self) -> &RctBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RctProofsTrait for RctProofs {
|
|
||||||
fn write(&self, w: &mut impl Write) -> io::Result<()> {
|
|
||||||
self.write(w)
|
|
||||||
}
|
|
||||||
fn read(
|
|
||||||
ring_length: usize,
|
|
||||||
inputs: usize,
|
|
||||||
outputs: usize,
|
|
||||||
r: &mut impl Read,
|
|
||||||
) -> io::Result<Option<Self>> {
|
|
||||||
RctProofs::read(ring_length, inputs, outputs, r)
|
|
||||||
}
|
|
||||||
fn rct_type(&self) -> RctType {
|
|
||||||
self.rct_type()
|
|
||||||
}
|
|
||||||
fn base(&self) -> &RctBase {
|
|
||||||
&self.base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RctProofsTrait for PrunedRctProofs {
|
|
||||||
fn write(&self, w: &mut impl Write) -> io::Result<()> {
|
|
||||||
self.base.write(w, self.rct_type)
|
|
||||||
}
|
|
||||||
fn read(
|
|
||||||
_ring_length: usize,
|
|
||||||
inputs: usize,
|
|
||||||
outputs: usize,
|
|
||||||
r: &mut impl Read,
|
|
||||||
) -> io::Result<Option<Self>> {
|
|
||||||
Ok(RctBase::read(inputs, outputs, r)?.map(|(rct_type, base)| Self { rct_type, base }))
|
|
||||||
}
|
|
||||||
fn rct_type(&self) -> RctType {
|
|
||||||
self.rct_type
|
|
||||||
}
|
|
||||||
fn base(&self) -> &RctBase {
|
|
||||||
&self.base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait PotentiallyPruned {
|
|
||||||
type RingSignatures: RingSignatures;
|
|
||||||
type RctProofs: RctProofsTrait;
|
|
||||||
}
|
|
||||||
/// A transaction which isn't pruned.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct NotPruned;
|
|
||||||
impl PotentiallyPruned for NotPruned {
|
|
||||||
type RingSignatures = Vec<RingSignature>;
|
|
||||||
type RctProofs = RctProofs;
|
|
||||||
}
|
|
||||||
/// A transaction which is pruned.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Pruned;
|
|
||||||
impl PotentiallyPruned for Pruned {
|
|
||||||
type RingSignatures = ();
|
|
||||||
type RctProofs = PrunedRctProofs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub use sealed::*;
|
|
||||||
|
|
||||||
/// A Monero transaction.
|
|
||||||
#[allow(private_bounds, private_interfaces, clippy::large_enum_variant)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum Transaction<P: PotentiallyPruned = NotPruned> {
|
|
||||||
/// A version 1 transaction, used by the original Cryptonote codebase.
|
|
||||||
V1 {
|
|
||||||
/// The transaction's prefix.
|
|
||||||
prefix: TransactionPrefix,
|
|
||||||
/// The transaction's ring signatures.
|
|
||||||
signatures: P::RingSignatures,
|
|
||||||
},
|
|
||||||
/// A version 2 transaction, used by the RingCT protocol.
|
|
||||||
V2 {
|
|
||||||
/// The transaction's prefix.
|
|
||||||
prefix: TransactionPrefix,
|
|
||||||
/// The transaction's proofs.
|
|
||||||
proofs: Option<P::RctProofs>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PrunableHash<'a> {
|
|
||||||
V1(&'a [RingSignature]),
|
|
||||||
V2([u8; 32]),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(private_bounds)]
|
|
||||||
impl<P: PotentiallyPruned> Transaction<P> {
|
|
||||||
/// Get the version of this transaction.
|
|
||||||
pub fn version(&self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { .. } => 1,
|
|
||||||
Transaction::V2 { .. } => 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the TransactionPrefix of this transaction.
|
|
||||||
pub fn prefix(&self) -> &TransactionPrefix {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to the TransactionPrefix of this transaction.
|
|
||||||
pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Transaction.
|
|
||||||
///
|
|
||||||
/// Some writable transactions may not be readable if they're malformed, per Monero's consensus
|
|
||||||
/// rules.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
write_varint(&self.version(), w)?;
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { prefix, signatures } => {
|
|
||||||
prefix.write(w)?;
|
|
||||||
for ring_sig in signatures.signatures_to_write() {
|
|
||||||
ring_sig.write(w)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Transaction::V2 { prefix, proofs } => {
|
|
||||||
prefix.write(w)?;
|
|
||||||
match proofs {
|
|
||||||
None => w.write_all(&[0])?,
|
|
||||||
Some(proofs) => proofs.write(w)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Transaction to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(2048);
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Transaction.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
let version = read_varint(r)?;
|
|
||||||
let prefix = TransactionPrefix::read(r, version)?;
|
|
||||||
|
|
||||||
if version == 1 {
|
|
||||||
let signatures = if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
|
|
||||||
Default::default()
|
|
||||||
} else {
|
|
||||||
P::RingSignatures::read_signatures(&prefix.inputs, r)?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Transaction::V1 { prefix, signatures })
|
|
||||||
} else if version == 2 {
|
|
||||||
let proofs = P::RctProofs::read(
|
|
||||||
prefix.inputs.first().map_or(0, |input| match input {
|
|
||||||
Input::Gen(_) => 0,
|
|
||||||
Input::ToKey { key_offsets, .. } => key_offsets.len(),
|
|
||||||
}),
|
|
||||||
prefix.inputs.len(),
|
|
||||||
prefix.outputs.len(),
|
|
||||||
r,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Transaction::V2 { prefix, proofs })
|
|
||||||
} else {
|
|
||||||
Err(io::Error::other("tried to deserialize unknown version"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hash of the transaction.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
fn hash_with_prunable_hash(&self, prunable: PrunableHash<'_>) -> [u8; 32] {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { prefix, .. } => {
|
|
||||||
let mut buf = Vec::with_capacity(512);
|
|
||||||
|
|
||||||
// We don't use `self.write` as that may write the signatures (if this isn't pruned)
|
|
||||||
write_varint(&self.version(), &mut buf)
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
prefix.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
|
|
||||||
// We explicitly write the signatures ourselves here
|
|
||||||
let PrunableHash::V1(signatures) = prunable else {
|
|
||||||
panic!("hashing v1 TX with non-v1 prunable data")
|
|
||||||
};
|
|
||||||
for signature in signatures {
|
|
||||||
signature.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
}
|
|
||||||
|
|
||||||
keccak256(buf)
|
|
||||||
}
|
|
||||||
Transaction::V2 { prefix, proofs } => {
|
|
||||||
let mut hashes = Vec::with_capacity(96);
|
|
||||||
|
|
||||||
hashes.extend(prefix.hash(2));
|
|
||||||
|
|
||||||
if let Some(proofs) = proofs {
|
|
||||||
let mut buf = Vec::with_capacity(512);
|
|
||||||
proofs
|
|
||||||
.base()
|
|
||||||
.write(&mut buf, proofs.rct_type())
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
hashes.extend(keccak256(&buf));
|
|
||||||
} else {
|
|
||||||
// Serialization of RctBase::Null
|
|
||||||
hashes.extend(keccak256([0]));
|
|
||||||
}
|
|
||||||
let PrunableHash::V2(prunable_hash) = prunable else {
|
|
||||||
panic!("hashing v2 TX with non-v2 prunable data")
|
|
||||||
};
|
|
||||||
hashes.extend(prunable_hash);
|
|
||||||
|
|
||||||
keccak256(hashes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Transaction<NotPruned> {
|
|
||||||
/// The hash of the transaction.
|
|
||||||
pub fn hash(&self) -> [u8; 32] {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { signatures, .. } => {
|
|
||||||
self.hash_with_prunable_hash(PrunableHash::V1(signatures))
|
|
||||||
}
|
|
||||||
Transaction::V2 { proofs, .. } => {
|
|
||||||
self.hash_with_prunable_hash(PrunableHash::V2(if let Some(proofs) = proofs {
|
|
||||||
let mut buf = Vec::with_capacity(1024);
|
|
||||||
proofs
|
|
||||||
.prunable
|
|
||||||
.write(&mut buf, proofs.rct_type())
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
keccak256(buf)
|
|
||||||
} else {
|
|
||||||
[0; 32]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the hash of this transaction as needed for signing it.
|
|
||||||
///
|
|
||||||
/// This returns None if the transaction is without signatures.
|
|
||||||
pub fn signature_hash(&self) -> Option<[u8; 32]> {
|
|
||||||
Some(match self {
|
|
||||||
Transaction::V1 { prefix, .. } => {
|
|
||||||
if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
self.hash_with_prunable_hash(PrunableHash::V1(&[]))
|
|
||||||
}
|
|
||||||
Transaction::V2 { proofs, .. } => self.hash_with_prunable_hash({
|
|
||||||
let Some(proofs) = proofs else { None? };
|
|
||||||
let mut buf = Vec::with_capacity(1024);
|
|
||||||
proofs
|
|
||||||
.prunable
|
|
||||||
.signature_write(&mut buf)
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
PrunableHash::V2(keccak256(buf))
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_rct_bulletproof(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { .. } => false,
|
|
||||||
Transaction::V2 { proofs, .. } => {
|
|
||||||
let Some(proofs) = proofs else { return false };
|
|
||||||
proofs.rct_type().bulletproof()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_rct_bulletproof_plus(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { .. } => false,
|
|
||||||
Transaction::V2 { proofs, .. } => {
|
|
||||||
let Some(proofs) = proofs else { return false };
|
|
||||||
proofs.rct_type().bulletproof_plus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the transaction's weight.
|
|
||||||
pub fn weight(&self) -> usize {
|
|
||||||
let blob_size = self.serialize().len();
|
|
||||||
|
|
||||||
let bp = self.is_rct_bulletproof();
|
|
||||||
let bp_plus = self.is_rct_bulletproof_plus();
|
|
||||||
if !(bp || bp_plus) {
|
|
||||||
blob_size
|
|
||||||
} else {
|
|
||||||
blob_size +
|
|
||||||
Bulletproof::calculate_clawback(
|
|
||||||
bp_plus,
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { .. } => panic!("v1 transaction was BP(+)"),
|
|
||||||
Transaction::V2 { prefix, .. } => prefix.outputs.len(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Transaction<NotPruned>> for Transaction<Pruned> {
|
|
||||||
fn from(tx: Transaction<NotPruned>) -> Transaction<Pruned> {
|
|
||||||
match tx {
|
|
||||||
Transaction::V1 { prefix, .. } => Transaction::V1 { prefix, signatures: () },
|
|
||||||
Transaction::V2 { prefix, proofs } => Transaction::V2 {
|
|
||||||
prefix,
|
|
||||||
proofs: proofs
|
|
||||||
.map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// TODO
|
|
||||||
#[test]
|
|
||||||
fn test() {}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-serai-verify-chain"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "A binary to deserialize and verify the Monero blockchain"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/verify-chain"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive", "alloc", "std"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["alloc", "std"] }
|
|
||||||
|
|
||||||
monero-serai = { path = "..", default-features = false, features = ["std", "compile-time-generators"] }
|
|
||||||
monero-rpc = { path = "../rpc", default-features = false, features = ["std"] }
|
|
||||||
monero-simple-request-rpc = { path = "../rpc/simple-request", default-features = false }
|
|
||||||
|
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# monero-serai Verify Chain
|
|
||||||
|
|
||||||
A binary to deserialize and verify the Monero blockchain.
|
|
||||||
|
|
||||||
This is not complete. This is not intended to be complete. This is intended to
|
|
||||||
test monero-serai against actual blockchain data. Do not use this as an
|
|
||||||
inflation checker.
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use monero_serai::{
|
|
||||||
io::decompress_point,
|
|
||||||
primitives::Commitment,
|
|
||||||
ringct::{RctPrunable, bulletproofs::BatchVerifier},
|
|
||||||
transaction::{Input, Transaction},
|
|
||||||
block::Block,
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_rpc::{RpcError, Rpc};
|
|
||||||
use monero_simple_request_rpc::SimpleRequestRpc;
|
|
||||||
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
|
|
||||||
async fn check_block(rpc: impl Rpc, block_i: usize) {
|
|
||||||
let hash = loop {
|
|
||||||
match rpc.get_block_hash(block_i).await {
|
|
||||||
Ok(hash) => break hash,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_block_hash ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't get block {block_i}'s hash: {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Grab the JSON to also check it was deserialized correctly
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct BlockResponse {
|
|
||||||
blob: String,
|
|
||||||
}
|
|
||||||
let res: BlockResponse = loop {
|
|
||||||
match rpc.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await {
|
|
||||||
Ok(res) => break res,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_block ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't get block {block_i} via block.hash(): {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let blob = hex::decode(res.blob).expect("node returned non-hex block");
|
|
||||||
let block = Block::read(&mut blob.as_slice())
|
|
||||||
.unwrap_or_else(|e| panic!("couldn't deserialize block {block_i}: {e}"));
|
|
||||||
assert_eq!(block.hash(), hash, "hash differs");
|
|
||||||
assert_eq!(block.serialize(), blob, "serialization differs");
|
|
||||||
|
|
||||||
let txs_len = 1 + block.transactions.len();
|
|
||||||
|
|
||||||
if !block.transactions.is_empty() {
|
|
||||||
// Test getting pruned transactions
|
|
||||||
loop {
|
|
||||||
match rpc.get_pruned_transactions(&block.transactions).await {
|
|
||||||
Ok(_) => break,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_pruned_transactions ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't call get_pruned_transactions: {e:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let txs = loop {
|
|
||||||
match rpc.get_transactions(&block.transactions).await {
|
|
||||||
Ok(txs) => break txs,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_transactions ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't call get_transactions: {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut batch = BatchVerifier::new();
|
|
||||||
for tx in txs {
|
|
||||||
match tx {
|
|
||||||
Transaction::V1 { prefix: _, signatures } => {
|
|
||||||
assert!(!signatures.is_empty());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Transaction::V2 { prefix: _, proofs: None } => {
|
|
||||||
panic!("proofs were empty in non-miner v2 transaction");
|
|
||||||
}
|
|
||||||
Transaction::V2 { ref prefix, proofs: Some(ref proofs) } => {
|
|
||||||
let sig_hash = tx.signature_hash().expect("no signature hash for TX with proofs");
|
|
||||||
// Verify all proofs we support proving for
|
|
||||||
// This is due to having debug_asserts calling verify within their proving, and CLSAG
|
|
||||||
// multisig explicitly calling verify as part of its signing process
|
|
||||||
// Accordingly, making sure our signature_hash algorithm is correct is great, and further
|
|
||||||
// making sure the verification functions are valid is appreciated
|
|
||||||
match &proofs.prunable {
|
|
||||||
RctPrunable::AggregateMlsagBorromean { .. } | RctPrunable::MlsagBorromean { .. } => {}
|
|
||||||
RctPrunable::MlsagBulletproofs { bulletproof, .. } |
|
|
||||||
RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, .. } => {
|
|
||||||
assert!(bulletproof.batch_verify(
|
|
||||||
&mut rand_core::OsRng,
|
|
||||||
&mut batch,
|
|
||||||
&proofs.base.commitments
|
|
||||||
));
|
|
||||||
}
|
|
||||||
RctPrunable::Clsag { bulletproof, clsags, pseudo_outs } => {
|
|
||||||
assert!(bulletproof.batch_verify(
|
|
||||||
&mut rand_core::OsRng,
|
|
||||||
&mut batch,
|
|
||||||
&proofs.base.commitments
|
|
||||||
));
|
|
||||||
|
|
||||||
for (i, clsag) in clsags.iter().enumerate() {
|
|
||||||
let (amount, key_offsets, image) = match &prefix.inputs[i] {
|
|
||||||
Input::Gen(_) => panic!("Input::Gen"),
|
|
||||||
Input::ToKey { amount, key_offsets, key_image } => {
|
|
||||||
(amount, key_offsets, key_image)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut running_sum = 0;
|
|
||||||
let mut actual_indexes = vec![];
|
|
||||||
for offset in key_offsets {
|
|
||||||
running_sum += offset;
|
|
||||||
actual_indexes.push(running_sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_outs(
|
|
||||||
rpc: &impl Rpc,
|
|
||||||
amount: u64,
|
|
||||||
indexes: &[u64],
|
|
||||||
) -> Vec<[EdwardsPoint; 2]> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Out {
|
|
||||||
key: String,
|
|
||||||
mask: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Outs {
|
|
||||||
outs: Vec<Out>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let outs: Outs = loop {
|
|
||||||
match rpc
|
|
||||||
.rpc_call(
|
|
||||||
"get_outs",
|
|
||||||
Some(json!({
|
|
||||||
"get_txid": true,
|
|
||||||
"outputs": indexes.iter().map(|o| json!({
|
|
||||||
"amount": amount,
|
|
||||||
"index": o
|
|
||||||
})).collect::<Vec<_>>()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(outs) => break outs,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_outs ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't connect to RPC to get outs: {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let rpc_point = |point: &str| {
|
|
||||||
decompress_point(
|
|
||||||
hex::decode(point)
|
|
||||||
.expect("invalid hex for ring member")
|
|
||||||
.try_into()
|
|
||||||
.expect("invalid point len for ring member"),
|
|
||||||
)
|
|
||||||
.expect("invalid point for ring member")
|
|
||||||
};
|
|
||||||
|
|
||||||
outs
|
|
||||||
.outs
|
|
||||||
.iter()
|
|
||||||
.map(|out| {
|
|
||||||
let mask = rpc_point(&out.mask);
|
|
||||||
if amount != 0 {
|
|
||||||
assert_eq!(mask, Commitment::new(Scalar::from(1u8), amount).calculate());
|
|
||||||
}
|
|
||||||
[rpc_point(&out.key), mask]
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
clsag
|
|
||||||
.verify(
|
|
||||||
&get_outs(&rpc, amount.unwrap_or(0), &actual_indexes).await,
|
|
||||||
image,
|
|
||||||
&pseudo_outs[i],
|
|
||||||
&sig_hash,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(batch.verify());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Deserialized, hashed, and reserialized {block_i} with {txs_len} TXs");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let args = std::env::args().collect::<Vec<String>>();
|
|
||||||
|
|
||||||
// Read start block as the first arg
|
|
||||||
let mut block_i =
|
|
||||||
args.get(1).expect("no start block specified").parse::<usize>().expect("invalid start block");
|
|
||||||
|
|
||||||
// How many blocks to work on at once
|
|
||||||
let async_parallelism: usize =
|
|
||||||
args.get(2).unwrap_or(&"8".to_string()).parse::<usize>().expect("invalid parallelism argument");
|
|
||||||
|
|
||||||
// Read further args as RPC URLs
|
|
||||||
let default_nodes = vec![
|
|
||||||
"http://xmr-node-uk.cakewallet.com:18081".to_string(),
|
|
||||||
"http://xmr-node-eu.cakewallet.com:18081".to_string(),
|
|
||||||
];
|
|
||||||
let mut specified_nodes = vec![];
|
|
||||||
{
|
|
||||||
let mut i = 0;
|
|
||||||
loop {
|
|
||||||
let Some(node) = args.get(3 + i) else { break };
|
|
||||||
specified_nodes.push(node.clone());
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let nodes = if specified_nodes.is_empty() { default_nodes } else { specified_nodes };
|
|
||||||
|
|
||||||
let rpc = |url: String| async move {
|
|
||||||
SimpleRequestRpc::new(url.clone())
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| panic!("couldn't create SimpleRequestRpc connected to {url}"))
|
|
||||||
};
|
|
||||||
let main_rpc = rpc(nodes[0].clone()).await;
|
|
||||||
let mut rpcs = vec![];
|
|
||||||
for i in 0 .. async_parallelism {
|
|
||||||
rpcs.push(rpc(nodes[i % nodes.len()].clone()).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rpc_i = 0;
|
|
||||||
let mut handles: Vec<JoinHandle<()>> = vec![];
|
|
||||||
let mut height = 0;
|
|
||||||
loop {
|
|
||||||
let new_height = main_rpc.get_height().await.expect("couldn't call get_height");
|
|
||||||
if new_height == height {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
height = new_height;
|
|
||||||
|
|
||||||
while block_i < height {
|
|
||||||
if handles.len() >= async_parallelism {
|
|
||||||
// Guarantee one handle is complete
|
|
||||||
handles.swap_remove(0).await.unwrap();
|
|
||||||
|
|
||||||
// Remove all of the finished handles
|
|
||||||
let mut i = 0;
|
|
||||||
while i < handles.len() {
|
|
||||||
if handles[i].is_finished() {
|
|
||||||
handles.swap_remove(i).await.unwrap();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handles.push(tokio::spawn(check_block(rpcs[rpc_i].clone(), block_i)));
|
|
||||||
rpc_i = (rpc_i + 1) % rpcs.len();
|
|
||||||
block_i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-wallet"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Wallet functionality for the Monero protocol, built around monero-serai"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.82"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.cargo-machete]
|
|
||||||
ignored = ["monero-clsag"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
# Used to send transactions
|
|
||||||
rand = { version = "0.8", default-features = false }
|
|
||||||
rand_chacha = { version = "0.3", default-features = false }
|
|
||||||
# Used to select decoys
|
|
||||||
rand_distr = { version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "group"] }
|
|
||||||
|
|
||||||
# Multisig dependencies
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
|
||||||
group = { version = "0.13", default-features = false, optional = true }
|
|
||||||
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false, optional = true }
|
|
||||||
frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true }
|
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
monero-clsag = { path = "../ringct/clsag", default-features = false }
|
|
||||||
monero-serai = { path = "..", default-features = false }
|
|
||||||
monero-rpc = { path = "../rpc", default-features = false }
|
|
||||||
monero-address = { path = "./address", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive", "alloc", "std"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["alloc", "std"] }
|
|
||||||
|
|
||||||
frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519", "tests"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", features = ["sync", "macros"] }
|
|
||||||
|
|
||||||
monero-simple-request-rpc = { path = "../rpc/simple-request", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
|
|
||||||
"rand_core/std",
|
|
||||||
"rand/std",
|
|
||||||
"rand_chacha/std",
|
|
||||||
"rand_distr/std",
|
|
||||||
|
|
||||||
"monero-clsag/std",
|
|
||||||
"monero-serai/std",
|
|
||||||
"monero-rpc/std",
|
|
||||||
"monero-address/std",
|
|
||||||
]
|
|
||||||
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-serai/compile-time-generators"]
|
|
||||||
multisig = ["std", "transcript", "group", "dalek-ff-group", "frost", "monero-clsag/multisig"]
|
|
||||||
default = ["std", "compile-time-generators"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# Monero Wallet
|
|
||||||
|
|
||||||
Wallet functionality for the Monero protocol, built around monero-serai. This
|
|
||||||
library prides itself on resolving common pit falls developers may face.
|
|
||||||
|
|
||||||
monero-wallet also offers a FROST-inspired multisignature protocol orders of
|
|
||||||
magnitude more performant than Monero's own.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Scanning Monero transactions
|
|
||||||
- Sending Monero transactions
|
|
||||||
- Sending Monero transactions with a FROST-inspired threshold multisignature
|
|
||||||
protocol, orders of magnitude more performant than Monero's own
|
|
||||||
|
|
||||||
### Caveats
|
|
||||||
|
|
||||||
This library DOES attempt to do the following:
|
|
||||||
|
|
||||||
- Create on-chain transactions identical to how wallet2 would (unless told not
|
|
||||||
to)
|
|
||||||
- Not be detectable as monero-serai when scanning outputs
|
|
||||||
- Not reveal spent outputs to the connected RPC node
|
|
||||||
|
|
||||||
This library DOES NOT attempt to do the following:
|
|
||||||
|
|
||||||
- Have identical RPC behavior when creating transactions
|
|
||||||
- Be a wallet
|
|
||||||
|
|
||||||
This means that monero-serai shouldn't be fingerprintable on-chain. It also
|
|
||||||
shouldn't be fingerprintable if a targeted attack occurs to detect if the
|
|
||||||
receiving wallet is monero-serai or wallet2. It also should be generally safe
|
|
||||||
for usage with remote nodes.
|
|
||||||
|
|
||||||
It won't hide from remote nodes it's monero-serai however, potentially
|
|
||||||
allowing a remote node to profile you. The implications of this are left to the
|
|
||||||
user to consider.
|
|
||||||
|
|
||||||
It also won't act as a wallet, just as a wallet functionality library. wallet2
|
|
||||||
has several *non-transaction-level* policies, such as always attempting to use
|
|
||||||
two inputs to create transactions. These are considered out of scope to
|
|
||||||
monero-serai.
|
|
||||||
|
|
||||||
Finally, this library only supports producing transactions with CLSAG
|
|
||||||
signatures. That means this library cannot spend non-RingCT outputs.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
- `compile-time-generators` (on by default): Derives the generators at
|
|
||||||
compile-time so they don't need to be derived at runtime. This is recommended
|
|
||||||
if program size doesn't need to be kept minimal.
|
|
||||||
- `multisig`: Adds support for creation of transactions using a threshold
|
|
||||||
multisignature wallet.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-address"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Rust implementation of Monero addresses"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/address"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
|
||||||
|
|
||||||
monero-io = { path = "../../io", default-features = false }
|
|
||||||
monero-primitives = { path = "../../primitives", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
hex-literal = { version = "0.4", default-features = false }
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
|
|
||||||
"monero-io/std",
|
|
||||||
]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Monero Address
|
|
||||||
|
|
||||||
Rust implementation of Monero addresses.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature (on by default) is
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
### Cargo Features
|
|
||||||
|
|
||||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
|
||||||
implementations).
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
use std_shims::{vec::Vec, string::String};
|
|
||||||
|
|
||||||
use monero_primitives::keccak256;
|
|
||||||
|
|
||||||
const ALPHABET_LEN: u64 = 58;
|
|
||||||
const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
||||||
|
|
||||||
pub(crate) const BLOCK_LEN: usize = 8;
|
|
||||||
const ENCODED_BLOCK_LEN: usize = 11;
|
|
||||||
|
|
||||||
const CHECKSUM_LEN: usize = 4;
|
|
||||||
|
|
||||||
// The maximum possible length of an encoding of this many bytes
|
|
||||||
//
|
|
||||||
// This is used for determining padding/how many bytes an encoding actually uses
|
|
||||||
pub(crate) fn encoded_len_for_bytes(bytes: usize) -> usize {
|
|
||||||
let bits = u64::try_from(bytes).expect("length exceeded 2**64") * 8;
|
|
||||||
let mut max = if bits == 64 { u64::MAX } else { (1 << bits) - 1 };
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
while max != 0 {
|
|
||||||
max /= ALPHABET_LEN;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode an arbitrary-length stream of data
|
|
||||||
pub(crate) fn encode(bytes: &[u8]) -> String {
|
|
||||||
let mut res = String::with_capacity(bytes.len().div_ceil(BLOCK_LEN) * ENCODED_BLOCK_LEN);
|
|
||||||
|
|
||||||
for chunk in bytes.chunks(BLOCK_LEN) {
|
|
||||||
// Convert to a u64
|
|
||||||
let mut fixed_len_chunk = [0; BLOCK_LEN];
|
|
||||||
fixed_len_chunk[(BLOCK_LEN - chunk.len()) ..].copy_from_slice(chunk);
|
|
||||||
let mut val = u64::from_be_bytes(fixed_len_chunk);
|
|
||||||
|
|
||||||
// Convert to the base58 encoding
|
|
||||||
let mut chunk_str = [char::from(ALPHABET[0]); ENCODED_BLOCK_LEN];
|
|
||||||
let mut i = 0;
|
|
||||||
while val > 0 {
|
|
||||||
chunk_str[i] = ALPHABET[usize::try_from(val % ALPHABET_LEN)
|
|
||||||
.expect("ALPHABET_LEN exceeds usize despite being a usize")]
|
|
||||||
.into();
|
|
||||||
i += 1;
|
|
||||||
val /= ALPHABET_LEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only take used bytes, and since we put the LSBs in the first byte, reverse the byte order
|
|
||||||
for c in chunk_str.into_iter().take(encoded_len_for_bytes(chunk.len())).rev() {
|
|
||||||
res.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode an arbitrary-length stream of data
|
|
||||||
pub(crate) fn decode(data: &str) -> Option<Vec<u8>> {
|
|
||||||
let mut res = Vec::with_capacity((data.len() / ENCODED_BLOCK_LEN) * BLOCK_LEN);
|
|
||||||
|
|
||||||
for chunk in data.as_bytes().chunks(ENCODED_BLOCK_LEN) {
|
|
||||||
// Convert the chunk back to a u64
|
|
||||||
let mut sum = 0u64;
|
|
||||||
for this_char in chunk {
|
|
||||||
sum = sum.checked_mul(ALPHABET_LEN)?;
|
|
||||||
sum += u64::try_from(ALPHABET.iter().position(|a| a == this_char)?)
|
|
||||||
.expect("alphabet len exceeded 2**64");
|
|
||||||
}
|
|
||||||
|
|
||||||
// From the size of the encoding, determine the size of the bytes
|
|
||||||
let mut used_bytes = None;
|
|
||||||
for i in 1 ..= BLOCK_LEN {
|
|
||||||
if encoded_len_for_bytes(i) == chunk.len() {
|
|
||||||
used_bytes = Some(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let used_bytes = used_bytes
|
|
||||||
.expect("chunk of bounded length exhaustively searched but couldn't find matching length");
|
|
||||||
// Only push on the used bytes
|
|
||||||
res.extend(&sum.to_be_bytes()[(BLOCK_LEN - used_bytes) ..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode an arbitrary-length stream of data, with a checksum
|
|
||||||
pub(crate) fn encode_check(mut data: Vec<u8>) -> String {
|
|
||||||
let checksum = keccak256(&data);
|
|
||||||
data.extend(&checksum[.. CHECKSUM_LEN]);
|
|
||||||
encode(&data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode an arbitrary-length stream of data, with a checksum
|
|
||||||
pub(crate) fn decode_check(data: &str) -> Option<Vec<u8>> {
|
|
||||||
let mut res = decode(data)?;
|
|
||||||
if res.len() < CHECKSUM_LEN {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
let checksum_pos = res.len() - CHECKSUM_LEN;
|
|
||||||
if keccak256(&res[.. checksum_pos])[.. CHECKSUM_LEN] != res[checksum_pos ..] {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
res.truncate(checksum_pos);
|
|
||||||
Some(res)
|
|
||||||
}
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use core::fmt::{self, Write};
|
|
||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
string::{String, ToString},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::EdwardsPoint;
|
|
||||||
|
|
||||||
use monero_io::*;
|
|
||||||
|
|
||||||
mod base58check;
|
|
||||||
use base58check::{encode_check, decode_check};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
/// The address type.
|
|
||||||
///
|
|
||||||
/// The officially specified addresses are supported, along with
|
|
||||||
/// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789).
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub enum AddressType {
|
|
||||||
/// A legacy address type.
|
|
||||||
Legacy,
|
|
||||||
/// A legacy address with a payment ID embedded.
|
|
||||||
LegacyIntegrated([u8; 8]),
|
|
||||||
/// A subaddress.
|
|
||||||
///
|
|
||||||
/// This is what SHOULD be used if specific functionality isn't needed.
|
|
||||||
Subaddress,
|
|
||||||
/// A featured address.
|
|
||||||
///
|
|
||||||
/// Featured Addresses are an unofficial address specification which is meant to be extensible
|
|
||||||
/// and support a variety of functionality. This functionality includes being a subaddresses AND
|
|
||||||
/// having a payment ID, along with being immune to the burning bug.
|
|
||||||
///
|
|
||||||
/// At this time, support for featured addresses is limited to this crate. There should be no
|
|
||||||
/// expectation of interoperability.
|
|
||||||
Featured {
|
|
||||||
/// If this address is a subaddress.
|
|
||||||
subaddress: bool,
|
|
||||||
/// The payment ID associated with this address.
|
|
||||||
payment_id: Option<[u8; 8]>,
|
|
||||||
/// If this address is guaranteed.
|
|
||||||
///
|
|
||||||
/// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
|
|
||||||
/// under the hardness of various cryptographic problems (which are assumed hard). This is via
|
|
||||||
/// a modified shared-key derivation which eliminates the burning bug.
|
|
||||||
guaranteed: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddressType {
|
|
||||||
/// If this address is a subaddress.
|
|
||||||
pub fn is_subaddress(&self) -> bool {
|
|
||||||
matches!(self, AddressType::Subaddress) ||
|
|
||||||
matches!(self, AddressType::Featured { subaddress: true, .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The payment ID within this address.
|
|
||||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
|
||||||
if let AddressType::LegacyIntegrated(id) = self {
|
|
||||||
Some(*id)
|
|
||||||
} else if let AddressType::Featured { payment_id, .. } = self {
|
|
||||||
*payment_id
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If this address is guaranteed.
|
|
||||||
///
|
|
||||||
/// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
|
|
||||||
/// under the hardness of various cryptographic problems (which are assumed hard). This is via
|
|
||||||
/// a modified shared-key derivation which eliminates the burning bug.
|
|
||||||
pub fn is_guaranteed(&self) -> bool {
|
|
||||||
matches!(self, AddressType::Featured { guaranteed: true, .. })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A subaddress index.
|
|
||||||
///
|
|
||||||
/// Subaddresses are derived from a root using a `(account, address)` tuple as an index.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct SubaddressIndex {
|
|
||||||
account: u32,
|
|
||||||
address: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SubaddressIndex {
|
|
||||||
/// Create a new SubaddressIndex.
|
|
||||||
pub const fn new(account: u32, address: u32) -> Option<SubaddressIndex> {
|
|
||||||
if (account == 0) && (address == 0) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(SubaddressIndex { account, address })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the account this subaddress index is under.
|
|
||||||
pub const fn account(&self) -> u32 {
|
|
||||||
self.account
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the address this subaddress index is for, within its account.
|
|
||||||
pub const fn address(&self) -> u32 {
|
|
||||||
self.address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bytes used as prefixes when encoding addresses.
|
|
||||||
///
|
|
||||||
/// These distinguish the address's type.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct AddressBytes {
|
|
||||||
legacy: u8,
|
|
||||||
legacy_integrated: u8,
|
|
||||||
subaddress: u8,
|
|
||||||
featured: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddressBytes {
|
|
||||||
/// Create a new set of address bytes, one for each address type.
|
|
||||||
pub const fn new(
|
|
||||||
legacy: u8,
|
|
||||||
legacy_integrated: u8,
|
|
||||||
subaddress: u8,
|
|
||||||
featured: u8,
|
|
||||||
) -> Option<Self> {
|
|
||||||
if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if (legacy_integrated == subaddress) || (legacy_integrated == featured) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if subaddress == featured {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(AddressBytes { legacy, legacy_integrated, subaddress, featured })
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn to_const_generic(self) -> u32 {
|
|
||||||
((self.legacy as u32) << 24) +
|
|
||||||
((self.legacy_integrated as u32) << 16) +
|
|
||||||
((self.subaddress as u32) << 8) +
|
|
||||||
(self.featured as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
const fn from_const_generic(const_generic: u32) -> Self {
|
|
||||||
let legacy = (const_generic >> 24) as u8;
|
|
||||||
let legacy_integrated = ((const_generic >> 16) & (u8::MAX as u32)) as u8;
|
|
||||||
let subaddress = ((const_generic >> 8) & (u8::MAX as u32)) as u8;
|
|
||||||
let featured = (const_generic & (u8::MAX as u32)) as u8;
|
|
||||||
|
|
||||||
AddressBytes { legacy, legacy_integrated, subaddress, featured }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
// /src/cryptonote_config.h#L216-L225
|
|
||||||
// https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789 for featured
|
|
||||||
const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) {
|
|
||||||
Some(bytes) => bytes,
|
|
||||||
None => panic!("mainnet byte constants conflicted"),
|
|
||||||
};
|
|
||||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
// /src/cryptonote_config.h#L277-L281
|
|
||||||
const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) {
|
|
||||||
Some(bytes) => bytes,
|
|
||||||
None => panic!("stagenet byte constants conflicted"),
|
|
||||||
};
|
|
||||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
// /src/cryptonote_config.h#L262-L266
|
|
||||||
const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) {
|
|
||||||
Some(bytes) => bytes,
|
|
||||||
None => panic!("testnet byte constants conflicted"),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The network this address is for.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub enum Network {
|
|
||||||
/// A mainnet address.
|
|
||||||
Mainnet,
|
|
||||||
/// A stagenet address.
|
|
||||||
///
|
|
||||||
/// Stagenet maintains parity with mainnet and is useful for testing integrations accordingly.
|
|
||||||
Stagenet,
|
|
||||||
/// A testnet address.
|
|
||||||
///
|
|
||||||
/// Testnet is used to test new consensus rules and functionality.
|
|
||||||
Testnet,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors when decoding an address.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum AddressError {
|
|
||||||
/// The address had an invalid (network, type) byte.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid byte for the address's network/type ({0})"))]
|
|
||||||
InvalidTypeByte(u8),
|
|
||||||
/// The address wasn't a valid Base58Check (as defined by Monero) string.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid address encoding"))]
|
|
||||||
InvalidEncoding,
|
|
||||||
/// The data encoded wasn't the proper length.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid length"))]
|
|
||||||
InvalidLength,
|
|
||||||
/// The address had an invalid key.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid key"))]
|
|
||||||
InvalidKey,
|
|
||||||
/// The address was featured with unrecognized features.
|
|
||||||
#[cfg_attr(feature = "std", error("unknown features"))]
|
|
||||||
UnknownFeatures(u64),
|
|
||||||
/// The network was for a different network than expected.
|
|
||||||
#[cfg_attr(
|
|
||||||
feature = "std",
|
|
||||||
error("different network ({actual:?}) than expected ({expected:?})")
|
|
||||||
)]
|
|
||||||
DifferentNetwork {
|
|
||||||
/// The Network expected.
|
|
||||||
expected: Network,
|
|
||||||
/// The Network embedded within the Address.
|
|
||||||
actual: Network,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bytes used as prefixes when encoding addresses, variable to the network instance.
|
|
||||||
///
|
|
||||||
/// These distinguish the address's network and type.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct NetworkedAddressBytes {
|
|
||||||
mainnet: AddressBytes,
|
|
||||||
stagenet: AddressBytes,
|
|
||||||
testnet: AddressBytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkedAddressBytes {
|
|
||||||
/// Create a new set of address bytes, one for each network.
|
|
||||||
pub const fn new(
|
|
||||||
mainnet: AddressBytes,
|
|
||||||
stagenet: AddressBytes,
|
|
||||||
testnet: AddressBytes,
|
|
||||||
) -> Option<Self> {
|
|
||||||
let res = NetworkedAddressBytes { mainnet, stagenet, testnet };
|
|
||||||
let all_bytes = res.to_const_generic();
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
while i < 12 {
|
|
||||||
let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128);
|
|
||||||
|
|
||||||
let mut j = 0;
|
|
||||||
while j < 12 {
|
|
||||||
if i == j {
|
|
||||||
j += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128);
|
|
||||||
if this_byte == other_byte {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert this set of address bytes to its representation as a u128.
|
|
||||||
///
|
|
||||||
/// We cannot use this struct directly as a const generic unfortunately.
|
|
||||||
pub const fn to_const_generic(self) -> u128 {
|
|
||||||
((self.mainnet.to_const_generic() as u128) << 96) +
|
|
||||||
((self.stagenet.to_const_generic() as u128) << 64) +
|
|
||||||
((self.testnet.to_const_generic() as u128) << 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
const fn from_const_generic(const_generic: u128) -> Self {
|
|
||||||
let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32);
|
|
||||||
let stagenet =
|
|
||||||
AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32);
|
|
||||||
let testnet =
|
|
||||||
AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32);
|
|
||||||
|
|
||||||
NetworkedAddressBytes { mainnet, stagenet, testnet }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn network(&self, network: Network) -> &AddressBytes {
|
|
||||||
match network {
|
|
||||||
Network::Mainnet => &self.mainnet,
|
|
||||||
Network::Stagenet => &self.stagenet,
|
|
||||||
Network::Testnet => &self.testnet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn byte(&self, network: Network, kind: AddressType) -> u8 {
|
|
||||||
let address_bytes = self.network(network);
|
|
||||||
|
|
||||||
match kind {
|
|
||||||
AddressType::Legacy => address_bytes.legacy,
|
|
||||||
AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated,
|
|
||||||
AddressType::Subaddress => address_bytes.subaddress,
|
|
||||||
AddressType::Featured { .. } => address_bytes.featured,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will return an incomplete AddressType for LegacyIntegrated/Featured.
|
|
||||||
fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> {
|
|
||||||
let mut meta = None;
|
|
||||||
for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
|
|
||||||
let address_bytes = self.network(network);
|
|
||||||
if let Some(kind) = match byte {
|
|
||||||
_ if byte == address_bytes.legacy => Some(AddressType::Legacy),
|
|
||||||
_ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])),
|
|
||||||
_ if byte == address_bytes.subaddress => Some(AddressType::Subaddress),
|
|
||||||
_ if byte == address_bytes.featured => {
|
|
||||||
Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
} {
|
|
||||||
meta = Some((network, kind));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
meta.ok_or(AddressError::InvalidTypeByte(byte))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bytes used for distinguishing Monero addresses.
|
|
||||||
pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new(
|
|
||||||
MONERO_MAINNET_BYTES,
|
|
||||||
MONERO_STAGENET_BYTES,
|
|
||||||
MONERO_TESTNET_BYTES,
|
|
||||||
) {
|
|
||||||
Some(bytes) => bytes,
|
|
||||||
None => panic!("Monero network byte constants conflicted"),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A Monero address.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
|
|
||||||
pub struct Address<const ADDRESS_BYTES: u128> {
|
|
||||||
network: Network,
|
|
||||||
kind: AddressType,
|
|
||||||
spend: EdwardsPoint,
|
|
||||||
view: EdwardsPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
|
||||||
let hex = |bytes: &[u8]| -> Result<String, fmt::Error> {
|
|
||||||
let mut res = String::with_capacity(2 + (2 * bytes.len()));
|
|
||||||
res.push_str("0x");
|
|
||||||
for b in bytes {
|
|
||||||
write!(&mut res, "{b:02x}")?;
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
|
|
||||||
fmt
|
|
||||||
.debug_struct("Address")
|
|
||||||
.field("network", &self.network)
|
|
||||||
.field("kind", &self.kind)
|
|
||||||
.field("spend", &hex(&self.spend.compress().to_bytes())?)
|
|
||||||
.field("view", &hex(&self.view.compress().to_bytes())?)
|
|
||||||
// This is not a real field yet is the most valuable thing to know when debugging
|
|
||||||
.field("(address)", &self.to_string())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let address_bytes: NetworkedAddressBytes =
|
|
||||||
NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
|
|
||||||
|
|
||||||
let mut data = vec![address_bytes.byte(self.network, self.kind)];
|
|
||||||
data.extend(self.spend.compress().to_bytes());
|
|
||||||
data.extend(self.view.compress().to_bytes());
|
|
||||||
if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind {
|
|
||||||
let features_uint =
|
|
||||||
(u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress);
|
|
||||||
write_varint(&features_uint, &mut data)
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
}
|
|
||||||
if let Some(id) = self.kind.payment_id() {
|
|
||||||
data.extend(id);
|
|
||||||
}
|
|
||||||
write!(f, "{}", encode_check(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
|
|
||||||
/// Create a new address.
|
|
||||||
pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
|
|
||||||
Address { network, kind, spend, view }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse an address from a String, accepting any network it is.
|
|
||||||
pub fn from_str_with_unchecked_network(s: &str) -> Result<Self, AddressError> {
|
|
||||||
let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?;
|
|
||||||
let mut raw = raw.as_slice();
|
|
||||||
|
|
||||||
let address_bytes: NetworkedAddressBytes =
|
|
||||||
NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
|
|
||||||
let (network, mut kind) = address_bytes
|
|
||||||
.metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?;
|
|
||||||
let spend = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
|
|
||||||
let view = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
|
|
||||||
|
|
||||||
if matches!(kind, AddressType::Featured { .. }) {
|
|
||||||
let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?;
|
|
||||||
if (features >> 3) != 0 {
|
|
||||||
Err(AddressError::UnknownFeatures(features))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let subaddress = (features & 1) == 1;
|
|
||||||
let integrated = ((features >> 1) & 1) == 1;
|
|
||||||
let guaranteed = ((features >> 2) & 1) == 1;
|
|
||||||
|
|
||||||
kind =
|
|
||||||
AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the payment ID, if there should be one
|
|
||||||
match kind {
|
|
||||||
AddressType::LegacyIntegrated(ref mut id) |
|
|
||||||
AddressType::Featured { payment_id: Some(ref mut id), .. } => {
|
|
||||||
*id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !raw.is_empty() {
|
|
||||||
Err(AddressError::InvalidLength)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Address { network, kind, spend, view })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new address from a `&str`.
|
|
||||||
///
|
|
||||||
/// This takes in an argument for the expected network, erroring if a distinct network was used.
|
|
||||||
/// It also errors if the address is invalid (as expected).
|
|
||||||
pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
|
|
||||||
Self::from_str_with_unchecked_network(s).and_then(|addr| {
|
|
||||||
if addr.network == network {
|
|
||||||
Ok(addr)
|
|
||||||
} else {
|
|
||||||
Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })?
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The network this address is intended for use on.
|
|
||||||
pub fn network(&self) -> Network {
|
|
||||||
self.network
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The type of address this is.
|
|
||||||
pub fn kind(&self) -> &AddressType {
|
|
||||||
&self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If this is a subaddress.
|
|
||||||
pub fn is_subaddress(&self) -> bool {
|
|
||||||
self.kind.is_subaddress()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The payment ID for this address.
|
|
||||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
|
||||||
self.kind.payment_id()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If this address is guaranteed.
|
|
||||||
///
|
|
||||||
/// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
|
|
||||||
/// under the hardness of various cryptographic problems (which are assumed hard). This is via
|
|
||||||
/// a modified shared-key derivation which eliminates the burning bug.
|
|
||||||
pub fn is_guaranteed(&self) -> bool {
|
|
||||||
self.kind.is_guaranteed()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The public spend key for this address.
|
|
||||||
pub fn spend(&self) -> EdwardsPoint {
|
|
||||||
self.spend
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The public view key for this address.
|
|
||||||
pub fn view(&self) -> EdwardsPoint {
|
|
||||||
self.view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Instantiation of the Address type with Monero's network bytes.
|
|
||||||
pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>;
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
use hex_literal::hex;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
|
||||||
|
|
||||||
use monero_io::decompress_point;
|
|
||||||
|
|
||||||
use crate::{Network, AddressType, MoneroAddress};
|
|
||||||
|
|
||||||
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
|
|
||||||
const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce");
|
|
||||||
|
|
||||||
const STANDARD: &str =
|
|
||||||
"4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
|
|
||||||
|
|
||||||
const PAYMENT_ID: [u8; 8] = hex!("b8963a57855cf73f");
|
|
||||||
const INTEGRATED: &str =
|
|
||||||
"4Ljin4CrSNHKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KbaTH6Mn\
|
|
||||||
pXSn88oBX35";
|
|
||||||
|
|
||||||
const SUB_SPEND: [u8; 32] =
|
|
||||||
hex!("fe358188b528335ad1cfdc24a22a23988d742c882b6f19a602892eaab3c1b62b");
|
|
||||||
const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a9517db1eea7794f5470");
|
|
||||||
const SUBADDRESS: &str =
|
|
||||||
"8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB";
|
|
||||||
|
|
||||||
const FEATURED_JSON: &str = include_str!("vectors/featured_addresses.json");
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_encoded_len_for_bytes() {
|
|
||||||
// For an encoding of length `l`, we prune to the amount of bytes which encodes with length `l`
|
|
||||||
// This assumes length `l` -> amount of bytes has a singular answer, which is tested here
|
|
||||||
use crate::base58check::*;
|
|
||||||
let mut set = std::collections::HashSet::new();
|
|
||||||
for i in 0 .. BLOCK_LEN {
|
|
||||||
set.insert(encoded_len_for_bytes(i));
|
|
||||||
}
|
|
||||||
assert_eq!(set.len(), BLOCK_LEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn base58check() {
|
|
||||||
use crate::base58check::*;
|
|
||||||
|
|
||||||
assert_eq!(encode(&[]), String::new());
|
|
||||||
assert!(decode("").unwrap().is_empty());
|
|
||||||
|
|
||||||
let full_block = &[1, 2, 3, 4, 5, 6, 7, 8];
|
|
||||||
assert_eq!(&decode(&encode(full_block)).unwrap(), full_block);
|
|
||||||
|
|
||||||
let partial_block = &[1, 2, 3];
|
|
||||||
assert_eq!(&decode(&encode(partial_block)).unwrap(), partial_block);
|
|
||||||
|
|
||||||
let max_encoded_block = &[u8::MAX; 8];
|
|
||||||
assert_eq!(&decode(&encode(max_encoded_block)).unwrap(), max_encoded_block);
|
|
||||||
|
|
||||||
let max_decoded_block = "zzzzzzzzzzz";
|
|
||||||
assert!(decode(max_decoded_block).is_none());
|
|
||||||
|
|
||||||
let full_and_partial_block = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
||||||
assert_eq!(&decode(&encode(full_and_partial_block)).unwrap(), full_and_partial_block);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn standard_address() {
|
|
||||||
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
|
|
||||||
assert_eq!(addr.network(), Network::Mainnet);
|
|
||||||
assert_eq!(addr.kind(), &AddressType::Legacy);
|
|
||||||
assert!(!addr.is_subaddress());
|
|
||||||
assert_eq!(addr.payment_id(), None);
|
|
||||||
assert!(!addr.is_guaranteed());
|
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
|
||||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
|
||||||
assert_eq!(addr.to_string(), STANDARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn integrated_address() {
|
|
||||||
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
|
|
||||||
assert_eq!(addr.network(), Network::Mainnet);
|
|
||||||
assert_eq!(addr.kind(), &AddressType::LegacyIntegrated(PAYMENT_ID));
|
|
||||||
assert!(!addr.is_subaddress());
|
|
||||||
assert_eq!(addr.payment_id(), Some(PAYMENT_ID));
|
|
||||||
assert!(!addr.is_guaranteed());
|
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
|
||||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
|
||||||
assert_eq!(addr.to_string(), INTEGRATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subaddress() {
|
|
||||||
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
|
|
||||||
assert_eq!(addr.network(), Network::Mainnet);
|
|
||||||
assert_eq!(addr.kind(), &AddressType::Subaddress);
|
|
||||||
assert!(addr.is_subaddress());
|
|
||||||
assert_eq!(addr.payment_id(), None);
|
|
||||||
assert!(!addr.is_guaranteed());
|
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
|
||||||
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
|
||||||
assert_eq!(addr.to_string(), SUBADDRESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn featured() {
|
|
||||||
for (network, first) in
|
|
||||||
[(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')]
|
|
||||||
{
|
|
||||||
for _ in 0 .. 100 {
|
|
||||||
let spend = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
|
||||||
let view = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
|
||||||
|
|
||||||
for features in 0 .. (1 << 3) {
|
|
||||||
const SUBADDRESS_FEATURE_BIT: u8 = 1;
|
|
||||||
const INTEGRATED_FEATURE_BIT: u8 = 1 << 1;
|
|
||||||
const GUARANTEED_FEATURE_BIT: u8 = 1 << 2;
|
|
||||||
|
|
||||||
let subaddress = (features & SUBADDRESS_FEATURE_BIT) == SUBADDRESS_FEATURE_BIT;
|
|
||||||
|
|
||||||
let mut payment_id = [0; 8];
|
|
||||||
OsRng.fill_bytes(&mut payment_id);
|
|
||||||
let payment_id = Some(payment_id)
|
|
||||||
.filter(|_| (features & INTEGRATED_FEATURE_BIT) == INTEGRATED_FEATURE_BIT);
|
|
||||||
|
|
||||||
let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT;
|
|
||||||
|
|
||||||
let kind = AddressType::Featured { subaddress, payment_id, guaranteed };
|
|
||||||
let addr = MoneroAddress::new(network, kind, spend, view);
|
|
||||||
|
|
||||||
assert_eq!(addr.to_string().chars().next().unwrap(), first);
|
|
||||||
assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr);
|
|
||||||
|
|
||||||
assert_eq!(addr.spend, spend);
|
|
||||||
assert_eq!(addr.view, view);
|
|
||||||
|
|
||||||
assert_eq!(addr.is_subaddress(), subaddress);
|
|
||||||
assert_eq!(addr.payment_id(), payment_id);
|
|
||||||
assert_eq!(addr.is_guaranteed(), guaranteed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn featured_vectors() {
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct Vector {
|
|
||||||
address: String,
|
|
||||||
|
|
||||||
network: String,
|
|
||||||
spend: String,
|
|
||||||
view: String,
|
|
||||||
|
|
||||||
subaddress: bool,
|
|
||||||
integrated: bool,
|
|
||||||
payment_id: Option<[u8; 8]>,
|
|
||||||
guaranteed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
let vectors = serde_json::from_str::<Vec<Vector>>(FEATURED_JSON).unwrap();
|
|
||||||
for vector in vectors {
|
|
||||||
let first = vector.address.chars().next().unwrap();
|
|
||||||
let network = match vector.network.as_str() {
|
|
||||||
"Mainnet" => {
|
|
||||||
assert_eq!(first, 'C');
|
|
||||||
Network::Mainnet
|
|
||||||
}
|
|
||||||
"Testnet" => {
|
|
||||||
assert_eq!(first, 'K');
|
|
||||||
Network::Testnet
|
|
||||||
}
|
|
||||||
"Stagenet" => {
|
|
||||||
assert_eq!(first, 'F');
|
|
||||||
Network::Stagenet
|
|
||||||
}
|
|
||||||
_ => panic!("Unknown network"),
|
|
||||||
};
|
|
||||||
let spend = decompress_point(hex::decode(vector.spend).unwrap().try_into().unwrap()).unwrap();
|
|
||||||
let view = decompress_point(hex::decode(vector.view).unwrap().try_into().unwrap()).unwrap();
|
|
||||||
|
|
||||||
let addr = MoneroAddress::from_str(network, &vector.address).unwrap();
|
|
||||||
assert_eq!(addr.spend, spend);
|
|
||||||
assert_eq!(addr.view, view);
|
|
||||||
|
|
||||||
assert_eq!(addr.is_subaddress(), vector.subaddress);
|
|
||||||
assert_eq!(vector.integrated, vector.payment_id.is_some());
|
|
||||||
assert_eq!(addr.payment_id(), vector.payment_id);
|
|
||||||
assert_eq!(addr.is_guaranteed(), vector.guaranteed);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
MoneroAddress::new(
|
|
||||||
network,
|
|
||||||
AddressType::Featured {
|
|
||||||
subaddress: vector.subaddress,
|
|
||||||
payment_id: vector.payment_id,
|
|
||||||
guaranteed: vector.guaranteed
|
|
||||||
},
|
|
||||||
spend,
|
|
||||||
view
|
|
||||||
)
|
|
||||||
.to_string(),
|
|
||||||
vector.address
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3pYyUDn",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3wfMHCy",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJTo4p5ayvj36PStM5AX",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [46, 48, 134, 34, 245, 148, 243, 195],
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJWv5WqMCNE2hRs9rJfy",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [153, 176, 98, 204, 151, 27, 197, 168],
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4DwqwH1",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4Pyz8bD",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJcwt7hykou237MqZZDA",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [88, 37, 149, 111, 171, 108, 120, 181],
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJfTrFAp69u2MYbf5YeN",
|
|
||||||
"network": "Mainnet",
|
|
||||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
|
||||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [125, 69, 155, 152, 140, 160, 157, 186],
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712U9w7ScYA",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UA2gCrT1",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc1DbPKwJu81cxJjqBkS",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [92, 225, 118, 220, 39, 3, 72, 51],
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc2o1rPMaXN31Fe5J6dn",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [20, 120, 47, 89, 72, 165, 233, 115],
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAQHCRZ4",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAUzqaii",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcAsfQc3gJQ2gHLd5DiQ",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [193, 149, 123, 214, 180, 205, 195, 91],
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcDBAD5jbZQ3AMHFyvQB",
|
|
||||||
"network": "Testnet",
|
|
||||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
|
||||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [205, 170, 65, 0, 51, 175, 251, 184],
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPJnBtTP",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPUrwMvP",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY5ECEhP5Nr1aCRPXdxk",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [173, 149, 78, 64, 215, 211, 66, 170],
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY882kTUS1D2LttnPvTR",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [254, 159, 186, 162, 1, 8, 156, 108],
|
|
||||||
"guaranteed": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPpBBo8F",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPuUJX3b",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": false,
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYCZPxVAoDu21DryMoto",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": false,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [3, 115, 230, 129, 172, 108, 116, 235],
|
|
||||||
"guaranteed": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYFYCqKQAWL18KkpBQ8R",
|
|
||||||
"network": "Stagenet",
|
|
||||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
|
||||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
|
||||||
"subaddress": true,
|
|
||||||
"integrated": true,
|
|
||||||
"payment_id": [94, 122, 63, 167, 209, 225, 14, 180],
|
|
||||||
"guaranteed": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
use std_shims::{io, vec::Vec, string::ToString, collections::HashSet};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
use rand_distr::{Distribution, Gamma};
|
|
||||||
#[cfg(not(feature = "std"))]
|
|
||||||
use rand_distr::num_traits::Float;
|
|
||||||
|
|
||||||
use curve25519_dalek::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
|
|
||||||
primitives::{Commitment, Decoys},
|
|
||||||
rpc::{RpcError, DecoyRpc},
|
|
||||||
output::OutputData,
|
|
||||||
WalletOutput,
|
|
||||||
};
|
|
||||||
|
|
||||||
const RECENT_WINDOW: u64 = 15;
|
|
||||||
const BLOCKS_PER_YEAR: usize = (365 * 24 * 60 * 60) / BLOCK_TIME;
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64;
|
|
||||||
|
|
||||||
async fn select_n(
|
|
||||||
rng: &mut (impl RngCore + CryptoRng),
|
|
||||||
rpc: &impl DecoyRpc,
|
|
||||||
height: usize,
|
|
||||||
real_output: u64,
|
|
||||||
ring_len: u8,
|
|
||||||
fingerprintable_deterministic: bool,
|
|
||||||
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
|
|
||||||
if height < DEFAULT_LOCK_WINDOW {
|
|
||||||
Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
|
|
||||||
}
|
|
||||||
if height > rpc.get_output_distribution_end_height().await? {
|
|
||||||
Err(RpcError::InternalError(
|
|
||||||
"decoys being requested from blocks this node doesn't have".to_string(),
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the distribution
|
|
||||||
let distribution = rpc.get_output_distribution(.. height).await?;
|
|
||||||
if distribution.len() < DEFAULT_LOCK_WINDOW {
|
|
||||||
Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
|
|
||||||
}
|
|
||||||
let highest_output_exclusive_bound = distribution[distribution.len() - DEFAULT_LOCK_WINDOW];
|
|
||||||
// This assumes that each miner TX had one output (as sane) and checks we have sufficient
|
|
||||||
// outputs even when excluding them (due to their own timelock requirements)
|
|
||||||
// Considering this a temporal error for very new chains, it's sufficiently sane to have
|
|
||||||
if highest_output_exclusive_bound.saturating_sub(
|
|
||||||
u64::try_from(COINBASE_LOCK_WINDOW).expect("coinbase lock window exceeds 2^{64}"),
|
|
||||||
) < u64::from(ring_len)
|
|
||||||
{
|
|
||||||
Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the outputs per second
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
let per_second = {
|
|
||||||
let blocks = distribution.len().min(BLOCKS_PER_YEAR);
|
|
||||||
let initial = distribution[distribution.len().saturating_sub(blocks + 1)];
|
|
||||||
let outputs = distribution[distribution.len() - 1].saturating_sub(initial);
|
|
||||||
(outputs as f64) / ((blocks * BLOCK_TIME) as f64)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Don't select the real output
|
|
||||||
let mut do_not_select = HashSet::new();
|
|
||||||
do_not_select.insert(real_output);
|
|
||||||
|
|
||||||
let decoy_count = usize::from(ring_len - 1);
|
|
||||||
let mut res = Vec::with_capacity(decoy_count);
|
|
||||||
|
|
||||||
let mut iters = 0;
|
|
||||||
// Iterates until we have enough decoys
|
|
||||||
// If an iteration only returns a partial set of decoys, the remainder will be obvious as decoys
|
|
||||||
// to the RPC
|
|
||||||
// The length of that remainder is expected to be minimal
|
|
||||||
while res.len() != decoy_count {
|
|
||||||
iters += 1;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
const MAX_ITERS: usize = 10;
|
|
||||||
// When testing on fresh chains, increased iterations can be useful and we don't necessitate
|
|
||||||
// reasonable performance
|
|
||||||
#[cfg(test)]
|
|
||||||
const MAX_ITERS: usize = 100;
|
|
||||||
// Ensure this isn't infinitely looping
|
|
||||||
// We check both that we aren't at the maximum amount of iterations and that the not-yet
|
|
||||||
// selected candidates exceed the amount of candidates necessary to trigger the next iteration
|
|
||||||
if (iters == MAX_ITERS) ||
|
|
||||||
((highest_output_exclusive_bound -
|
|
||||||
u64::try_from(do_not_select.len()).expect("amount of ignored decoys exceeds 2^{64}")) <
|
|
||||||
u64::from(ring_len))
|
|
||||||
{
|
|
||||||
Err(RpcError::InternalError("hit decoy selection round limit".to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining = decoy_count - res.len();
|
|
||||||
let mut candidates = Vec::with_capacity(remaining);
|
|
||||||
while candidates.len() != remaining {
|
|
||||||
// Use a gamma distribution, as Monero does
|
|
||||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
|
|
||||||
// /src/wallet/wallet2.cpp#L142-L143
|
|
||||||
let mut age = Gamma::<f64>::new(19.28, 1.0 / 1.61)
|
|
||||||
.expect("constant Gamma distribution could no longer be created")
|
|
||||||
.sample(rng)
|
|
||||||
.exp();
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
if age > TIP_APPLICATION {
|
|
||||||
age -= TIP_APPLICATION;
|
|
||||||
} else {
|
|
||||||
// f64 does not have try_from available, which is why these are written with `as`
|
|
||||||
age = (rng.next_u64() %
|
|
||||||
(RECENT_WINDOW * u64::try_from(BLOCK_TIME).expect("BLOCK_TIME exceeded u64::MAX")))
|
|
||||||
as f64;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
|
||||||
let o = (age * per_second) as u64;
|
|
||||||
if o < highest_output_exclusive_bound {
|
|
||||||
// Find which block this points to
|
|
||||||
let i = distribution.partition_point(|s| *s < (highest_output_exclusive_bound - 1 - o));
|
|
||||||
let prev = i.saturating_sub(1);
|
|
||||||
let n = distribution[i].checked_sub(distribution[prev]).ok_or_else(|| {
|
|
||||||
RpcError::InternalError("RPC returned non-monotonic distribution".to_string())
|
|
||||||
})?;
|
|
||||||
if n != 0 {
|
|
||||||
// Select an output from within this block
|
|
||||||
let o = distribution[prev] + (rng.next_u64() % n);
|
|
||||||
if !do_not_select.contains(&o) {
|
|
||||||
candidates.push(o);
|
|
||||||
// This output will either be used or is unusable
|
|
||||||
// In either case, we should not try it again
|
|
||||||
do_not_select.insert(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the first time we're requesting these outputs, include the real one as well
|
|
||||||
// Prevents the node we're connected to from having a list of known decoys and then seeing a
|
|
||||||
// TX which uses all of them, with one additional output (the true spend)
|
|
||||||
let real_index = if iters == 0 {
|
|
||||||
candidates.push(real_output);
|
|
||||||
// Sort candidates so the real spends aren't the ones at the end
|
|
||||||
candidates.sort();
|
|
||||||
Some(
|
|
||||||
candidates
|
|
||||||
.binary_search(&real_output)
|
|
||||||
.expect("selected a ring which didn't include the real spend"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
for (i, output) in rpc
|
|
||||||
.get_unlocked_outputs(&candidates, height, fingerprintable_deterministic)
|
|
||||||
.await?
|
|
||||||
.iter_mut()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
// We could check the returned info is equivalent to our expectations, yet that'd allow the
|
|
||||||
// node to malleate the returned info to see if they can cause this error (allowing them to
|
|
||||||
// figure out the output being spent)
|
|
||||||
//
|
|
||||||
// Some degree of this attack (forcing resampling/trying to observe errors) is likely
|
|
||||||
// always possible
|
|
||||||
if real_index == Some(i) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is an unlocked output, push it to the result
|
|
||||||
if let Some(output) = output.take() {
|
|
||||||
res.push((candidates[i], output));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn select_decoys<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
rpc: &impl DecoyRpc,
|
|
||||||
ring_len: u8,
|
|
||||||
height: usize,
|
|
||||||
input: &WalletOutput,
|
|
||||||
fingerprintable_deterministic: bool,
|
|
||||||
) -> Result<Decoys, RpcError> {
|
|
||||||
if ring_len == 0 {
|
|
||||||
Err(RpcError::InternalError("requesting a ring of length 0".to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select all decoys for this transaction, assuming we generate a sane transaction
|
|
||||||
// We should almost never naturally generate an insane transaction, hence why this doesn't
|
|
||||||
// bother with an overage
|
|
||||||
let decoys = select_n(
|
|
||||||
rng,
|
|
||||||
rpc,
|
|
||||||
height,
|
|
||||||
input.relative_id.index_on_blockchain,
|
|
||||||
ring_len,
|
|
||||||
fingerprintable_deterministic,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Form the complete ring
|
|
||||||
let mut ring = decoys;
|
|
||||||
ring.push((input.relative_id.index_on_blockchain, [input.key(), input.commitment().calculate()]));
|
|
||||||
ring.sort_by(|a, b| a.0.cmp(&b.0));
|
|
||||||
|
|
||||||
/*
|
|
||||||
Monero does have sanity checks which it applies to the selected ring.
|
|
||||||
|
|
||||||
They're statistically unlikely to be hit and only occur when the transaction is published over
|
|
||||||
the RPC (so they are not a relay rule). The RPC allows disabling them, which monero-rpc does to
|
|
||||||
ensure they don't pose a problem.
|
|
||||||
|
|
||||||
They aren't worth the complexity to implement here, especially since they're non-deterministic.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// We need to convert our positional indexes to offset indexes
|
|
||||||
let mut offsets = Vec::with_capacity(ring.len());
|
|
||||||
{
|
|
||||||
offsets.push(ring[0].0);
|
|
||||||
for m in 1 .. ring.len() {
|
|
||||||
offsets.push(ring[m].0 - ring[m - 1].0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
Decoys::new(
|
|
||||||
offsets,
|
|
||||||
// Binary searches for the real spend since we don't know where it sorted to
|
|
||||||
// TODO: Define our own collection whose `len` function returns `u8` to ensure this bound
|
|
||||||
// with types
|
|
||||||
u8::try_from(ring.partition_point(|x| x.0 < input.relative_id.index_on_blockchain))
|
|
||||||
.expect("ring of size <= u8::MAX had an index exceeding u8::MAX"),
|
|
||||||
ring.into_iter().map(|output| output.1).collect(),
|
|
||||||
)
|
|
||||||
.expect("selected a syntactically-invalid set of Decoys"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An output with decoys selected.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct OutputWithDecoys {
|
|
||||||
output: OutputData,
|
|
||||||
decoys: Decoys,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputWithDecoys {
|
|
||||||
/// Select decoys for this output.
|
|
||||||
pub async fn new(
|
|
||||||
rng: &mut (impl Send + Sync + RngCore + CryptoRng),
|
|
||||||
rpc: &impl DecoyRpc,
|
|
||||||
ring_len: u8,
|
|
||||||
height: usize,
|
|
||||||
output: WalletOutput,
|
|
||||||
) -> Result<OutputWithDecoys, RpcError> {
|
|
||||||
let decoys = select_decoys(rng, rpc, ring_len, height, &output, false).await?;
|
|
||||||
Ok(OutputWithDecoys { output: output.data.clone(), decoys })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select a set of decoys for this output with a deterministic process.
|
|
||||||
///
|
|
||||||
/// This function will always output the same set of decoys when called with the same arguments.
|
|
||||||
/// This makes it very useful in multisignature contexts, where instead of having one participant
|
|
||||||
/// select the decoys, everyone can locally select the decoys while coming to the same result.
|
|
||||||
///
|
|
||||||
/// The set of decoys selected may be fingerprintable as having been produced by this
|
|
||||||
/// methodology.
|
|
||||||
pub async fn fingerprintable_deterministic_new(
|
|
||||||
rng: &mut (impl Send + Sync + RngCore + CryptoRng),
|
|
||||||
rpc: &impl DecoyRpc,
|
|
||||||
ring_len: u8,
|
|
||||||
height: usize,
|
|
||||||
output: WalletOutput,
|
|
||||||
) -> Result<OutputWithDecoys, RpcError> {
|
|
||||||
let decoys = select_decoys(rng, rpc, ring_len, height, &output, true).await?;
|
|
||||||
Ok(OutputWithDecoys { output: output.data.clone(), decoys })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The key this output may be spent by.
|
|
||||||
pub fn key(&self) -> EdwardsPoint {
|
|
||||||
self.output.key()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scalar to add to the private spend key for it to be the discrete logarithm of this
|
|
||||||
/// output's key.
|
|
||||||
pub fn key_offset(&self) -> Scalar {
|
|
||||||
self.output.key_offset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The commitment this output created.
|
|
||||||
pub fn commitment(&self) -> &Commitment {
|
|
||||||
&self.output.commitment
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The decoys this output selected.
|
|
||||||
pub fn decoys(&self) -> &Decoys {
|
|
||||||
&self.decoys
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the OutputWithDecoys.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.output.write(w)?;
|
|
||||||
self.decoys.write(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the OutputWithDecoys to a `Vec<u8>`.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = Vec::with_capacity(128);
|
|
||||||
self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an OutputWithDecoys.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn read<R: io::Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
Ok(Self { output: OutputData::read(r)?, decoys: Decoys::read(r)? })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
use core::ops::BitXor;
|
|
||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, BufRead, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
|
||||||
|
|
||||||
use monero_serai::io::*;
|
|
||||||
|
|
||||||
pub(crate) const MAX_TX_EXTRA_PADDING_COUNT: usize = 255;
|
|
||||||
const MAX_TX_EXTRA_NONCE_SIZE: usize = 255;
|
|
||||||
|
|
||||||
const PAYMENT_ID_MARKER: u8 = 0;
|
|
||||||
const ENCRYPTED_PAYMENT_ID_MARKER: u8 = 1;
|
|
||||||
// Used as it's the highest value not interpretable as a continued VarInt
|
|
||||||
pub(crate) const ARBITRARY_DATA_MARKER: u8 = 127;
|
|
||||||
|
|
||||||
/// The max amount of data which will fit within a blob of arbitrary data.
|
|
||||||
// 1 byte is used for the marker
|
|
||||||
pub const MAX_ARBITRARY_DATA_SIZE: usize = MAX_TX_EXTRA_NONCE_SIZE - 1;
|
|
||||||
|
|
||||||
/// A Payment ID.
|
|
||||||
///
|
|
||||||
/// This is a legacy method of identifying why Monero was sent to the receiver.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub enum PaymentId {
|
|
||||||
/// A deprecated form of payment ID which is no longer supported.
|
|
||||||
Unencrypted([u8; 32]),
|
|
||||||
/// An encrypted payment ID.
|
|
||||||
Encrypted([u8; 8]),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BitXor<[u8; 8]> for PaymentId {
|
|
||||||
type Output = PaymentId;
|
|
||||||
|
|
||||||
fn bitxor(self, bytes: [u8; 8]) -> PaymentId {
|
|
||||||
match self {
|
|
||||||
// Don't perform the xor since this isn't intended to be encrypted with xor
|
|
||||||
PaymentId::Unencrypted(_) => self,
|
|
||||||
PaymentId::Encrypted(id) => {
|
|
||||||
PaymentId::Encrypted((u64::from_le_bytes(id) ^ u64::from_le_bytes(bytes)).to_le_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaymentId {
|
|
||||||
/// Write the PaymentId.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
PaymentId::Unencrypted(id) => {
|
|
||||||
w.write_all(&[PAYMENT_ID_MARKER])?;
|
|
||||||
w.write_all(id)?;
|
|
||||||
}
|
|
||||||
PaymentId::Encrypted(id) => {
|
|
||||||
w.write_all(&[ENCRYPTED_PAYMENT_ID_MARKER])?;
|
|
||||||
w.write_all(id)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the PaymentId to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(1 + 8);
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a PaymentId.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<PaymentId> {
|
|
||||||
Ok(match read_byte(r)? {
|
|
||||||
0 => PaymentId::Unencrypted(read_bytes(r)?),
|
|
||||||
1 => PaymentId::Encrypted(read_bytes(r)?),
|
|
||||||
_ => Err(io::Error::other("unknown payment ID type"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A field within the TX extra.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub enum ExtraField {
|
|
||||||
/// Padding.
|
|
||||||
///
|
|
||||||
/// This is a block of zeroes within the TX extra.
|
|
||||||
Padding(usize),
|
|
||||||
/// The transaction key.
|
|
||||||
///
|
|
||||||
/// This is a commitment to the randomness used for deriving outputs.
|
|
||||||
PublicKey(EdwardsPoint),
|
|
||||||
/// The nonce field.
|
|
||||||
///
|
|
||||||
/// This is used for data, such as payment IDs.
|
|
||||||
Nonce(Vec<u8>),
|
|
||||||
/// The field for merge-mining.
|
|
||||||
///
|
|
||||||
/// This is used within miner transactions who are merge-mining Monero to specify the foreign
|
|
||||||
/// block they mined.
|
|
||||||
MergeMining(u64, [u8; 32]),
|
|
||||||
/// The additional transaction keys.
|
|
||||||
///
|
|
||||||
/// These are the per-output commitments to the randomness used for deriving outputs.
|
|
||||||
PublicKeys(Vec<EdwardsPoint>),
|
|
||||||
/// The 'mysterious' Minergate tag.
|
|
||||||
///
|
|
||||||
/// This was used by a closed source entity without documentation. Support for parsing it was
|
|
||||||
/// added to reduce extra which couldn't be decoded.
|
|
||||||
MysteriousMinergate(Vec<u8>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtraField {
|
|
||||||
/// Write the ExtraField.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
ExtraField::Padding(size) => {
|
|
||||||
w.write_all(&[0])?;
|
|
||||||
for _ in 1 .. *size {
|
|
||||||
write_byte(&0u8, w)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ExtraField::PublicKey(key) => {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
w.write_all(&key.compress().to_bytes())?;
|
|
||||||
}
|
|
||||||
ExtraField::Nonce(data) => {
|
|
||||||
w.write_all(&[2])?;
|
|
||||||
write_vec(write_byte, data, w)?;
|
|
||||||
}
|
|
||||||
ExtraField::MergeMining(height, merkle) => {
|
|
||||||
w.write_all(&[3])?;
|
|
||||||
write_varint(height, w)?;
|
|
||||||
w.write_all(merkle)?;
|
|
||||||
}
|
|
||||||
ExtraField::PublicKeys(keys) => {
|
|
||||||
w.write_all(&[4])?;
|
|
||||||
write_vec(write_point, keys, w)?;
|
|
||||||
}
|
|
||||||
ExtraField::MysteriousMinergate(data) => {
|
|
||||||
w.write_all(&[0xDE])?;
|
|
||||||
write_vec(write_byte, data, w)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the ExtraField to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(1 + 8);
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an ExtraField.
|
|
||||||
pub fn read<R: BufRead>(r: &mut R) -> io::Result<ExtraField> {
|
|
||||||
Ok(match read_byte(r)? {
|
|
||||||
0 => ExtraField::Padding({
|
|
||||||
// Read until either non-zero, max padding count, or end of buffer
|
|
||||||
let mut size: usize = 1;
|
|
||||||
loop {
|
|
||||||
let buf = r.fill_buf()?;
|
|
||||||
let mut n_consume = 0;
|
|
||||||
for v in buf {
|
|
||||||
if *v != 0u8 {
|
|
||||||
Err(io::Error::other("non-zero value after padding"))?
|
|
||||||
}
|
|
||||||
n_consume += 1;
|
|
||||||
size += 1;
|
|
||||||
if size > MAX_TX_EXTRA_PADDING_COUNT {
|
|
||||||
Err(io::Error::other("padding exceeded max count"))?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n_consume == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
r.consume(n_consume);
|
|
||||||
}
|
|
||||||
size
|
|
||||||
}),
|
|
||||||
1 => ExtraField::PublicKey(read_point(r)?),
|
|
||||||
2 => ExtraField::Nonce(read_vec(read_byte, Some(MAX_TX_EXTRA_NONCE_SIZE), r)?),
|
|
||||||
3 => ExtraField::MergeMining(read_varint(r)?, read_bytes(r)?),
|
|
||||||
4 => ExtraField::PublicKeys(read_vec(read_point, None, r)?),
|
|
||||||
0xDE => ExtraField::MysteriousMinergate(read_vec(read_byte, None, r)?),
|
|
||||||
_ => Err(io::Error::other("unknown extra field"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of decoding a transaction's extra field.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct Extra(pub(crate) Vec<ExtraField>);
|
|
||||||
impl Extra {
|
|
||||||
/// The keys within this extra.
|
|
||||||
///
|
|
||||||
/// This returns all keys specified with `PublicKey` and the first set of keys specified with
|
|
||||||
/// `PublicKeys`, so long as they're well-formed.
|
|
||||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
|
|
||||||
// /src/wallet/wallet2.cpp#L2290-L2300
|
|
||||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
// /src/wallet/wallet2.cpp#L2337-L2340
|
|
||||||
pub fn keys(&self) -> Option<(Vec<EdwardsPoint>, Option<Vec<EdwardsPoint>>)> {
|
|
||||||
let mut keys = vec![];
|
|
||||||
let mut additional = None;
|
|
||||||
for field in &self.0 {
|
|
||||||
match field.clone() {
|
|
||||||
ExtraField::PublicKey(this_key) => keys.push(this_key),
|
|
||||||
ExtraField::PublicKeys(these_additional) => {
|
|
||||||
additional = additional.or(Some(these_additional))
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Don't return any keys if this was non-standard and didn't include the primary key
|
|
||||||
if keys.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((keys, additional))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The payment ID embedded within this extra.
|
|
||||||
// Monero finds the first nonce field and reads the payment ID from it:
|
|
||||||
// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
|
|
||||||
// src/wallet/wallet2.cpp#L2709-L2752
|
|
||||||
pub fn payment_id(&self) -> Option<PaymentId> {
|
|
||||||
for field in &self.0 {
|
|
||||||
if let ExtraField::Nonce(data) = field {
|
|
||||||
return PaymentId::read::<&[u8]>(&mut data.as_ref()).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The arbitrary data within this extra.
|
|
||||||
///
|
|
||||||
/// This uses a marker custom to monero-wallet.
|
|
||||||
pub fn data(&self) -> Vec<Vec<u8>> {
|
|
||||||
let mut res = vec![];
|
|
||||||
for field in &self.0 {
|
|
||||||
if let ExtraField::Nonce(data) = field {
|
|
||||||
if data[0] == ARBITRARY_DATA_MARKER {
|
|
||||||
res.push(data[1 ..].to_vec());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn new(key: EdwardsPoint, additional: Vec<EdwardsPoint>) -> Extra {
|
|
||||||
let mut res = Extra(Vec::with_capacity(3));
|
|
||||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
|
||||||
// /src/cryptonote_basic/cryptonote_format_utils.cpp#L627-L633
|
|
||||||
// We only support pushing nonces which come after these in the sort order
|
|
||||||
res.0.push(ExtraField::PublicKey(key));
|
|
||||||
if !additional.is_empty() {
|
|
||||||
res.0.push(ExtraField::PublicKeys(additional));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn push_nonce(&mut self, nonce: Vec<u8>) {
|
|
||||||
self.0.push(ExtraField::Nonce(nonce));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Extra.
|
|
||||||
///
|
|
||||||
/// This is not of deterministic length nor length-prefixed. It should only be written to a
|
|
||||||
/// buffer which will be delimited.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for field in &self.0 {
|
|
||||||
field.write(w)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the Extra to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut buf = vec![];
|
|
||||||
self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an `Extra`.
|
|
||||||
///
|
|
||||||
/// This is not of deterministic length nor length-prefixed. It should only be read from a buffer
|
|
||||||
/// already delimited.
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
pub fn read<R: BufRead>(r: &mut R) -> io::Result<Extra> {
|
|
||||||
let mut res = Extra(vec![]);
|
|
||||||
// Extra reads until EOF
|
|
||||||
// We take a BufRead so we can detect when the buffer is empty
|
|
||||||
// `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
|
|
||||||
// exhausted
|
|
||||||
while !r.fill_buf()?.is_empty() {
|
|
||||||
let Ok(field) = ExtraField::read(r) else { break };
|
|
||||||
res.0.push(field);
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
|
|
||||||
use curve25519_dalek::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_serai::{
|
|
||||||
io::write_varint,
|
|
||||||
primitives::{Commitment, keccak256, keccak256_to_scalar},
|
|
||||||
ringct::EncryptedAmount,
|
|
||||||
transaction::Input,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use monero_serai::*;
|
|
||||||
|
|
||||||
pub use monero_rpc as rpc;
|
|
||||||
|
|
||||||
pub use monero_address as address;
|
|
||||||
|
|
||||||
mod view_pair;
|
|
||||||
pub use view_pair::{ViewPairError, ViewPair, GuaranteedViewPair};
|
|
||||||
|
|
||||||
/// Structures and functionality for working with transactions' extra fields.
|
|
||||||
pub mod extra;
|
|
||||||
pub(crate) use extra::{PaymentId, Extra};
|
|
||||||
|
|
||||||
pub(crate) mod output;
|
|
||||||
pub use output::WalletOutput;
|
|
||||||
|
|
||||||
mod scan;
|
|
||||||
pub use scan::{Timelocked, ScanError, Scanner, GuaranteedScanner};
|
|
||||||
|
|
||||||
mod decoys;
|
|
||||||
pub use decoys::OutputWithDecoys;
|
|
||||||
|
|
||||||
/// Structs and functionality for sending transactions.
|
|
||||||
pub mod send;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
|
||||||
struct SharedKeyDerivations {
|
|
||||||
// Hs("view_tag" || 8Ra || o)
|
|
||||||
view_tag: u8,
|
|
||||||
// Hs(uniqueness || 8Ra || o) where uniqueness may be empty
|
|
||||||
shared_key: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SharedKeyDerivations {
|
|
||||||
// https://gist.github.com/kayabaNerve/8066c13f1fe1573286ba7a2fd79f6100
|
|
||||||
fn uniqueness(inputs: &[Input]) -> [u8; 32] {
|
|
||||||
let mut u = b"uniqueness".to_vec();
|
|
||||||
for input in inputs {
|
|
||||||
match input {
|
|
||||||
// If Gen, this should be the only input, making this loop somewhat pointless
|
|
||||||
// This works and even if there were somehow multiple inputs, it'd be a false negative
|
|
||||||
Input::Gen(height) => {
|
|
||||||
write_varint(height, &mut u).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
}
|
|
||||||
Input::ToKey { key_image, .. } => u.extend(key_image.compress().to_bytes()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keccak256(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
fn output_derivations(
|
|
||||||
uniqueness: Option<[u8; 32]>,
|
|
||||||
ecdh: Zeroizing<EdwardsPoint>,
|
|
||||||
o: usize,
|
|
||||||
) -> Zeroizing<SharedKeyDerivations> {
|
|
||||||
// 8Ra
|
|
||||||
let mut output_derivation = Zeroizing::new(
|
|
||||||
Zeroizing::new(Zeroizing::new(ecdh.mul_by_cofactor()).compress().to_bytes()).to_vec(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// || o
|
|
||||||
{
|
|
||||||
let output_derivation: &mut Vec<u8> = output_derivation.as_mut();
|
|
||||||
write_varint(&o, output_derivation)
|
|
||||||
.expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
}
|
|
||||||
|
|
||||||
let view_tag = keccak256([b"view_tag".as_ref(), &output_derivation].concat())[0];
|
|
||||||
|
|
||||||
// uniqueness ||
|
|
||||||
let output_derivation = if let Some(uniqueness) = uniqueness {
|
|
||||||
Zeroizing::new([uniqueness.as_ref(), &output_derivation].concat())
|
|
||||||
} else {
|
|
||||||
output_derivation
|
|
||||||
};
|
|
||||||
|
|
||||||
Zeroizing::new(SharedKeyDerivations {
|
|
||||||
view_tag,
|
|
||||||
shared_key: keccak256_to_scalar(&output_derivation),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// H(8Ra || 0x8d)
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
fn payment_id_xor(ecdh: Zeroizing<EdwardsPoint>) -> [u8; 8] {
|
|
||||||
// 8Ra
|
|
||||||
let output_derivation = Zeroizing::new(
|
|
||||||
Zeroizing::new(Zeroizing::new(ecdh.mul_by_cofactor()).compress().to_bytes()).to_vec(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut payment_id_xor = [0; 8];
|
|
||||||
payment_id_xor
|
|
||||||
.copy_from_slice(&keccak256([output_derivation.as_ref(), [0x8d].as_ref()].concat())[.. 8]);
|
|
||||||
payment_id_xor
|
|
||||||
}
|
|
||||||
|
|
||||||
fn commitment_mask(&self) -> Scalar {
|
|
||||||
let mut mask = b"commitment_mask".to_vec();
|
|
||||||
mask.extend(self.shared_key.as_bytes());
|
|
||||||
let res = keccak256_to_scalar(&mask);
|
|
||||||
mask.zeroize();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compact_amount_encryption(&self, amount: u64) -> [u8; 8] {
|
|
||||||
let mut amount_mask = Zeroizing::new(b"amount".to_vec());
|
|
||||||
amount_mask.extend(self.shared_key.to_bytes());
|
|
||||||
let mut amount_mask = keccak256(&amount_mask);
|
|
||||||
|
|
||||||
let mut amount_mask_8 = [0; 8];
|
|
||||||
amount_mask_8.copy_from_slice(&amount_mask[.. 8]);
|
|
||||||
amount_mask.zeroize();
|
|
||||||
|
|
||||||
(amount ^ u64::from_le_bytes(amount_mask_8)).to_le_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment {
|
|
||||||
match enc_amount {
|
|
||||||
EncryptedAmount::Original { mask, amount } => {
|
|
||||||
let mask_shared_sec_scalar = keccak256_to_scalar(self.shared_key.as_bytes());
|
|
||||||
let amount_shared_sec_scalar = keccak256_to_scalar(mask_shared_sec_scalar.as_bytes());
|
|
||||||
|
|
||||||
let mask = Scalar::from_bytes_mod_order(*mask) - mask_shared_sec_scalar;
|
|
||||||
let amount_scalar = Scalar::from_bytes_mod_order(*amount) - amount_shared_sec_scalar;
|
|
||||||
|
|
||||||
// d2b from rctTypes.cpp
|
|
||||||
let amount = u64::from_le_bytes(
|
|
||||||
amount_scalar.to_bytes()[.. 8]
|
|
||||||
.try_into()
|
|
||||||
.expect("32-byte array couldn't have an 8-byte slice taken"),
|
|
||||||
);
|
|
||||||
|
|
||||||
Commitment::new(mask, amount)
|
|
||||||
}
|
|
||||||
EncryptedAmount::Compact { amount } => Commitment::new(
|
|
||||||
self.commitment_mask(),
|
|
||||||
u64::from_le_bytes(self.compact_amount_encryption(u64::from_le_bytes(*amount))),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use curve25519_dalek::{Scalar, edwards::EdwardsPoint};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
io::*, primitives::Commitment, transaction::Timelock, address::SubaddressIndex, extra::PaymentId,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An absolute output ID, defined as its transaction hash and output index.
|
|
||||||
///
|
|
||||||
/// This is not the output's key as multiple outputs may share an output key.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct AbsoluteId {
|
|
||||||
pub(crate) transaction: [u8; 32],
|
|
||||||
pub(crate) index_in_transaction: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for AbsoluteId {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt
|
|
||||||
.debug_struct("AbsoluteId")
|
|
||||||
.field("transaction", &hex::encode(self.transaction))
|
|
||||||
.field("index_in_transaction", &self.index_in_transaction)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AbsoluteId {
|
|
||||||
/// Write the AbsoluteId.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.transaction)?;
|
|
||||||
w.write_all(&self.index_in_transaction.to_le_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an AbsoluteId.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
fn read<R: Read>(r: &mut R) -> io::Result<AbsoluteId> {
|
|
||||||
Ok(AbsoluteId { transaction: read_bytes(r)?, index_in_transaction: read_u64(r)? })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An output's relative ID.
|
|
||||||
///
|
|
||||||
/// This is defined as the output's index on the blockchain.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct RelativeId {
|
|
||||||
pub(crate) index_on_blockchain: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for RelativeId {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt.debug_struct("RelativeId").field("index_on_blockchain", &self.index_on_blockchain).finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RelativeId {
|
|
||||||
/// Write the RelativeId.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.index_on_blockchain.to_le_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an RelativeId.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
fn read<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
Ok(RelativeId { index_on_blockchain: read_u64(r)? })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The data within an output, as necessary to spend the output.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct OutputData {
|
|
||||||
pub(crate) key: EdwardsPoint,
|
|
||||||
pub(crate) key_offset: Scalar,
|
|
||||||
pub(crate) commitment: Commitment,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for OutputData {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt
|
|
||||||
.debug_struct("OutputData")
|
|
||||||
.field("key", &hex::encode(self.key.compress().0))
|
|
||||||
.field("key_offset", &hex::encode(self.key_offset.to_bytes()))
|
|
||||||
.field("commitment", &self.commitment)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputData {
|
|
||||||
/// The key this output may be spent by.
|
|
||||||
pub(crate) fn key(&self) -> EdwardsPoint {
|
|
||||||
self.key
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scalar to add to the private spend key for it to be the discrete logarithm of this
|
|
||||||
/// output's key.
|
|
||||||
pub(crate) fn key_offset(&self) -> Scalar {
|
|
||||||
self.key_offset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The commitment this output created.
|
|
||||||
pub(crate) fn commitment(&self) -> &Commitment {
|
|
||||||
&self.commitment
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the OutputData.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.key.compress().to_bytes())?;
|
|
||||||
w.write_all(&self.key_offset.to_bytes())?;
|
|
||||||
self.commitment.write(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Commented as it's unused, due to self being private
|
|
||||||
/// Serialize the OutputData to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(32 + 32 + 40);
|
|
||||||
self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
res
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// Read an OutputData.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub(crate) fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
|
|
||||||
Ok(OutputData {
|
|
||||||
key: read_point(r)?,
|
|
||||||
key_offset: read_scalar(r)?,
|
|
||||||
commitment: Commitment::read(r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The metadata for an output.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct Metadata {
|
|
||||||
pub(crate) additional_timelock: Timelock,
|
|
||||||
pub(crate) subaddress: Option<SubaddressIndex>,
|
|
||||||
pub(crate) payment_id: Option<PaymentId>,
|
|
||||||
pub(crate) arbitrary_data: Vec<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for Metadata {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt
|
|
||||||
.debug_struct("Metadata")
|
|
||||||
.field("additional_timelock", &self.additional_timelock)
|
|
||||||
.field("subaddress", &self.subaddress)
|
|
||||||
.field("payment_id", &self.payment_id)
|
|
||||||
.field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Metadata {
|
|
||||||
/// Write the Metadata.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.additional_timelock.write(w)?;
|
|
||||||
|
|
||||||
if let Some(subaddress) = self.subaddress {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
w.write_all(&subaddress.account().to_le_bytes())?;
|
|
||||||
w.write_all(&subaddress.address().to_le_bytes())?;
|
|
||||||
} else {
|
|
||||||
w.write_all(&[0])?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(payment_id) = self.payment_id {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
payment_id.write(w)?;
|
|
||||||
} else {
|
|
||||||
w.write_all(&[0])?;
|
|
||||||
}
|
|
||||||
|
|
||||||
w.write_all(
|
|
||||||
&u64::try_from(self.arbitrary_data.len())
|
|
||||||
.expect("amount of arbitrary data chunks exceeded u64::MAX")
|
|
||||||
.to_le_bytes(),
|
|
||||||
)?;
|
|
||||||
for part in &self.arbitrary_data {
|
|
||||||
// TODO: Define our own collection whose `len` function returns `u8` to ensure this bound
|
|
||||||
// with types
|
|
||||||
w.write_all(&[
|
|
||||||
u8::try_from(part.len()).expect("piece of arbitrary data exceeded max length of u8::MAX")
|
|
||||||
])?;
|
|
||||||
w.write_all(part)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Metadata.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
|
|
||||||
let additional_timelock = Timelock::read(r)?;
|
|
||||||
|
|
||||||
let subaddress = match read_byte(r)? {
|
|
||||||
0 => None,
|
|
||||||
1 => Some(
|
|
||||||
SubaddressIndex::new(read_u32(r)?, read_u32(r)?)
|
|
||||||
.ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?,
|
|
||||||
),
|
|
||||||
_ => Err(io::Error::other("invalid subaddress is_some boolean in metadata"))?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Metadata {
|
|
||||||
additional_timelock,
|
|
||||||
subaddress,
|
|
||||||
payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
|
|
||||||
arbitrary_data: {
|
|
||||||
let mut data = vec![];
|
|
||||||
for _ in 0 .. read_u64(r)? {
|
|
||||||
let len = read_byte(r)?;
|
|
||||||
data.push(read_raw_vec(read_byte, usize::from(len), r)?);
|
|
||||||
}
|
|
||||||
data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A scanned output and all associated data.
|
|
||||||
///
|
|
||||||
/// This struct contains all data necessary to spend this output, or handle it as a payment.
|
|
||||||
///
|
|
||||||
/// This struct is bound to a specific instance of the blockchain. If the blockchain reorganizes
|
|
||||||
/// the block this struct is bound to, it MUST be discarded. If any outputs are mutual to both
|
|
||||||
/// blockchains, scanning the new blockchain will yield those outputs again.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct WalletOutput {
|
|
||||||
/// The absolute ID for this transaction.
|
|
||||||
pub(crate) absolute_id: AbsoluteId,
|
|
||||||
/// The ID for this transaction, relative to the blockchain.
|
|
||||||
pub(crate) relative_id: RelativeId,
|
|
||||||
/// The output's data.
|
|
||||||
pub(crate) data: OutputData,
|
|
||||||
/// Associated metadata relevant for handling it as a payment.
|
|
||||||
pub(crate) metadata: Metadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletOutput {
|
|
||||||
/// The hash of the transaction which created this output.
|
|
||||||
pub fn transaction(&self) -> [u8; 32] {
|
|
||||||
self.absolute_id.transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The index of the output within the transaction.
|
|
||||||
pub fn index_in_transaction(&self) -> u64 {
|
|
||||||
self.absolute_id.index_in_transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The index of the output on the blockchain.
|
|
||||||
pub fn index_on_blockchain(&self) -> u64 {
|
|
||||||
self.relative_id.index_on_blockchain
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The key this output may be spent by.
|
|
||||||
pub fn key(&self) -> EdwardsPoint {
|
|
||||||
self.data.key()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scalar to add to the private spend key for it to be the discrete logarithm of this
|
|
||||||
/// output's key.
|
|
||||||
pub fn key_offset(&self) -> Scalar {
|
|
||||||
self.data.key_offset()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The commitment this output created.
|
|
||||||
pub fn commitment(&self) -> &Commitment {
|
|
||||||
self.data.commitment()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The additional timelock this output is subject to.
|
|
||||||
///
|
|
||||||
/// All outputs are subject to the '10-block lock', a 10-block window after their inclusion
|
|
||||||
/// on-chain during which they cannot be spent. Outputs may be additionally timelocked. This
|
|
||||||
/// function only returns the additional timelock.
|
|
||||||
pub fn additional_timelock(&self) -> Timelock {
|
|
||||||
self.metadata.additional_timelock
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The index of the subaddress this output was identified as sent to.
|
|
||||||
pub fn subaddress(&self) -> Option<SubaddressIndex> {
|
|
||||||
self.metadata.subaddress
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The payment ID included with this output.
|
|
||||||
///
|
|
||||||
/// This field may be `Some` even if wallet2 would not return a payment ID. wallet2 will only
|
|
||||||
/// decrypt a payment ID if either:
|
|
||||||
///
|
|
||||||
/// A) The transaction wasn't made by the wallet (via checking if any key images are recognized)
|
|
||||||
/// B) For the highest-indexed input with a recognized key image, it spends an output with
|
|
||||||
/// subaddress account `(a, _)` which is distinct from this output's subaddress account
|
|
||||||
///
|
|
||||||
/// Neither of these cases are handled by `monero-wallet` as scanning doesn't have the context
|
|
||||||
/// of key images.
|
|
||||||
//
|
|
||||||
// Identification of the subaddress account for the highest-indexed input with a recognized key
|
|
||||||
// image:
|
|
||||||
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
|
||||||
// /src/wallet/wallet2.cpp/#L2637-L2670
|
|
||||||
//
|
|
||||||
// Removal of 'transfers' received to this account:
|
|
||||||
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
|
||||||
// /src/wallet/wallet2.cpp/#L2782-L2794
|
|
||||||
//
|
|
||||||
// Payment IDs only being decrypted for the remaining transfers:
|
|
||||||
// https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
|
|
||||||
// /src/wallet/wallet2.cpp/#L2796-L2844
|
|
||||||
pub fn payment_id(&self) -> Option<PaymentId> {
|
|
||||||
self.metadata.payment_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The arbitrary data from the `extra` field of the transaction which created this output.
|
|
||||||
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
|
|
||||||
&self.metadata.arbitrary_data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the WalletOutput.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.absolute_id.write(w)?;
|
|
||||||
self.relative_id.write(w)?;
|
|
||||||
self.data.write(w)?;
|
|
||||||
self.metadata.write(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize the WalletOutput to a `Vec<u8>`.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = Vec::with_capacity(128);
|
|
||||||
self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a WalletOutput.
|
|
||||||
///
|
|
||||||
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
|
||||||
/// defined serialization.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<WalletOutput> {
|
|
||||||
Ok(WalletOutput {
|
|
||||||
absolute_id: AbsoluteId::read(r)?,
|
|
||||||
relative_id: RelativeId::read(r)?,
|
|
||||||
data: OutputData::read(r)?,
|
|
||||||
metadata: Metadata::read(r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user