diff --git a/Cargo.lock b/Cargo.lock
index 1bb4713..b671a68 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -955,6 +955,21 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+[[package]]
+name = "function_name"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1ab577a896d09940b5fe12ec5ae71f9d8211fff62c919c03a3750a9901e98a7"
+dependencies = [
+ "function_name-proc-macro",
+]
+
+[[package]]
+name = "function_name-proc-macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673464e1e314dd67a0fd9544abc99e8eb28d0c7e3b69b033bcff9b2d00b87333"
+
[[package]]
name = "futures"
version = "0.1.31"
@@ -1339,6 +1354,42 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "grin_onion"
+version = "0.1.0"
+dependencies = [
+ "blake2-rfc",
+ "byteorder",
+ "bytes 0.5.6",
+ "chacha20",
+ "curve25519-dalek 2.1.3",
+ "ed25519-dalek",
+ "grin_api 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
+ "grin_chain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
+ "grin_core 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
+ "grin_keychain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
+ "grin_secp256k1zkp",
+ "grin_servers",
+ "grin_store 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
+ "grin_util 5.1.1",
+ "grin_wallet_api",
+ "grin_wallet_impls",
+ "grin_wallet_libwallet",
+ "grin_wallet_util",
+ "hmac 0.12.0",
+ "itertools",
+ "lazy_static",
+ "rand 0.7.3",
+ "rpassword",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha2 0.10.0",
+ "thiserror",
+ "toml",
+ "x25519-dalek 0.6.0",
+]
+
[[package]]
name = "grin_p2p"
version = "5.2.0-alpha.1"
@@ -1737,6 +1788,31 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+[[package]]
+name = "headers"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
+dependencies = [
+ "base64 0.13.0",
+ "bitflags 1.3.2",
+ "bytes 1.1.0",
+ "headers-core",
+ "http",
+ "httpdate 1.0.1",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+dependencies = [
+ "http",
+]
+
[[package]]
name = "heck"
version = "0.3.3"
@@ -1897,6 +1973,24 @@ dependencies = [
"want",
]
+[[package]]
+name = "hyper-proxy"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc"
+dependencies = [
+ "bytes 1.1.0",
+ "futures 0.3.17",
+ "headers",
+ "http",
+ "hyper 0.14.14",
+ "hyper-tls 0.5.0",
+ "native-tls",
+ "tokio 1.12.0",
+ "tokio-native-tls",
+ "tower-service",
+]
+
[[package]]
name = "hyper-rustls"
version = "0.20.0"
@@ -1956,6 +2050,19 @@ dependencies = [
"tokio-tls",
]
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes 1.1.0",
+ "hyper 0.14.14",
+ "native-tls",
+ "tokio 1.12.0",
+ "tokio-native-tls",
+]
+
[[package]]
name = "i18n-config"
version = "0.4.2"
@@ -2478,11 +2585,13 @@ dependencies = [
"curve25519-dalek 2.1.3",
"dirs",
"ed25519-dalek",
+ "function_name",
"futures 0.3.17",
"grin_api 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
"grin_chain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
"grin_core 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
"grin_keychain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
+ "grin_onion",
"grin_secp256k1zkp",
"grin_servers",
"grin_store 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
@@ -2493,6 +2602,7 @@ dependencies = [
"grin_wallet_util",
"hmac 0.12.0",
"hyper 0.14.14",
+ "hyper-proxy",
"itertools",
"jsonrpc-core 18.0.0",
"jsonrpc-derive",
@@ -3280,7 +3390,7 @@ dependencies = [
"http-body 0.3.1",
"hyper 0.13.10",
"hyper-rustls 0.21.0",
- "hyper-tls",
+ "hyper-tls 0.4.3",
"ipnet",
"js-sys",
"lazy_static",
@@ -3634,6 +3744,17 @@ dependencies = [
"yaml-rust 0.4.5",
]
+[[package]]
+name = "sha1"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04cc229fb94bcb689ffc39bd4ded842f6ff76885efede7c6d1ffb62582878bea"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.10.1",
+]
+
[[package]]
name = "sha2"
version = "0.8.2"
@@ -4019,6 +4140,16 @@ dependencies = [
"syn 1.0.84",
]
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio 1.12.0",
+]
+
[[package]]
name = "tokio-rustls"
version = "0.13.1"
diff --git a/Cargo.toml b/Cargo.toml
index 7078fb5..fbd4750 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[workspace]
+members = ["onion"]
+
[dependencies]
blake2 = { package = "blake2-rfc", version = "0.2"}
byteorder = "1"
@@ -14,9 +17,11 @@ clap = { version = "2.33", features = ["yaml"] }
curve25519-dalek = "2.1"
dirs = "2.0"
ed25519-dalek = "1.0.1"
+function_name = "0.3.0"
futures = "0.3"
hmac = { version = "0.12.0", features = ["std"]}
hyper = { version = "0.14", features = ["full"] }
+hyper-proxy = "0.9.1"
itertools = { version = "0.10.3"}
jsonrpc-core = "18.0"
jsonrpc-derive = "18.0"
@@ -34,6 +39,7 @@ thiserror = "1.0.31"
tokio = { version = "1", features = ["full"] }
toml = "0.5"
x25519-dalek = "0.6.0"
+grin_onion = { path = "./onion" }
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
grin_util = "5"
grin_api = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
diff --git a/README.md b/README.md
index 3a2a1fc..e9efe52 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,11 @@
# MWixnet
This is an implementation of @tromp's [CoinSwap Proposal](https://forum.grin.mw/t/mimblewimble-coinswap-proposal/8322) with some slight modifications.
-A set of n CoinSwap servers (nodei with i=1...n) are agreed upon in advance. They each have a known public key.
+A set of n CoinSwap servers (Ni with i=1...n) are agreed upon in advance. They each have a known public key.
+
+We refer to the first server (N1) as the "Swap Server." This is the server that wallets can submit their coinswaps too.
+
+We refer to the remaining servers (N2...Nn) as "Mixers."
### Setup
#### init-config
@@ -19,7 +23,7 @@ A grin-wallet account must be created for receiving extra mwixnet fees. The wall
With your wallet and fully synced node both online and listening at the addresses configured, the mwixnet server can be started by running `mwixnet` and providing the server key password and wallet password when prompted.
### SWAP API
-The first CoinSwap server (n1) provides the `swap` API, publicly available for use by GRIN wallets.
+The Swap Server (N1) provides the `swap` API, which is publicly available for use by GRIN wallets.
**jsonrpc:** `2.0`
**method:** `swap`
diff --git a/doc/onion.md b/doc/onion.md
new file mode 100644
index 0000000..f14251f
--- /dev/null
+++ b/doc/onion.md
@@ -0,0 +1,202 @@
+# Onion Routing
+
+## Overview
+
+The Onion Routing scheme in this context is a method of encrypting and routing transactions in a privacy-preserving manner. At each step, a server peels off a layer of encryption, revealing information intended for it, and passes on the rest of the data to the next server.
+
+The main component in this encryption scheme is an `Onion`, which contains encrypted payloads, an ephemeral public key and an output commitment.
+
+## Data Structures
+
+### `Hop` structure
+
+Each `Hop` represents a step in the routing process, which has its own unique encryption parameters. A `Hop` consists of:
+
+- `server_pubkey`: The public key of the server for this hop.
+- `excess`: An additional blinding factor to add to the commitment.
+- `fee`: The transaction fee for this hop.
+- `rangeproof`: An optional rangeproof, included only for the final hop.
+
+### `Onion` structure
+
+An `Onion` represents a complete route at a particular stage. It contains:
+
+- `ephemeral_pubkey`: The ephemeral public key for the server that is next in line to peel off a layer of the onion.
+- `commit`: The modified commitment at this stage in the routing process, which will be the original commitment for the first server in the chain, and then will be a recalculated commitment at each following stage.
+- `enc_payloads`: The list of encrypted payloads for each remaining hop in the route.
+
+The `commit` in an `Onion` will be the original unspent output's commitment for the very first `Onion` object sent to the swap server, but then for each peeled layer (i.e., after each hop), a new `Onion` object will be created with a recalculated commitment. This new commitment reflects the additional blinding factor and subtracted fee at each stage. The `Onion` passed from one server to the next then contains this adjusted commitment, not the original one.
+
+### `Payload` structure
+
+Each encrypted payload contains the information needed by a server to process the hop. This includes:
+
+- `next_ephemeral_pk`: The ephemeral public key for the next hop.
+- `excess`: The additional blinding factor for the commitment at this hop.
+- `fee`: The transaction fee for this hop.
+- `rangeproof`: A rangeproof if the payload is for the final hop.
+ Absolutely, let's go into more detail on the cryptographic methods utilized during the creation and peeling of the Onion.
+
+### Creating an Onion
+
+The creation of the Onion involves both symmetric and asymmetric encryption techniques:
+
+1. **Ephemeral keys:** For each hop (server) in the network, an ephemeral secret key is randomly generated. These ephemeral keys are used to create shared secrets with the server's public key through the Diffie-Hellman key exchange. The first ephemeral public key is included in the Onion, and each subsequent ephemeral public key is encrypted and included in the payload for the previous server.
+
+2. **Shared secrets:** A shared secret is generated between the sender (the client) and each server (hop) in the path. This is done using the Elliptic Curve Diffie-Hellman (ECDH) method. The shared secret for each server is calculated from the server's public key and the client's ephemeral secret key.
+
+3. **Payload encryption:** Detailed in the next section.
+
+### Payload Encryption with ChaCha20 Cipher
+
+After the shared secrets are created, they are used to derive keys for symmetric encryption with the ChaCha20 cipher. Here is the process:
+
+1. **Key derivation:** An HMAC-SHA-256 is used as a key derivation function (KDF). The shared secret is fed into this HMAC function with a constant key of "MWIXNET". The HMAC is used here as a pseudo-random function (PRF) to derive a 256-bit key from the shared secret. The purpose of using HMAC in this manner is to ensure that the output key is indistinguishable from random data, assuming the shared secret is also indistinguishable from random data. The output of the HMAC function is a 256-bit key.
+
+2. **Nonce:** A nonce is a random or pseudo-random value that is meant to prevent replay attacks. For the ChaCha20 cipher, a 12-byte nonce is used. In this case, a static nonce of "NONCE1234567" is used. This means that the security of the cipher relies solely on the never-reusing any key more than once, since the nonce is not being used as an input of randomness.
+
+3. **ChaCha20 Initialization**: The derived key and static nonce are used to initialize the ChaCha20 cipher.
+
+4. **Payload Encryption** Each server's payload is encrypted with all the shared secrets of that server and all previous servers, in reverse order. This means the payload for the first server is encrypted once, the second server's payload is encrypted twice, and so on, creating the layered "onion" encryption.
+
+```rust
+fn new_stream_cipher(shared_secret: &SharedSecret) -> Result {
+ let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?;
+ mu_hmac.update(shared_secret.as_bytes());
+ let mukey = mu_hmac.finalize().into_bytes();
+
+ let key = Key::from_slice(&mukey[0..32]);
+ let nonce = Nonce::from_slice(b"NONCE1234567");
+
+ Ok(ChaCha20::new(&key, &nonce))
+}
+```
+
+### Peeling an Onion
+
+The peeling of the Onion is basically the decryption process that happens at each server:
+
+1. **Shared secret:** The server creates a shared secret using its own private key and the client's ephemeral public key.
+
+2. **Decryption:** The server uses the shared secret to derive a key for the ChaCha20 cipher. It then uses this to decrypt the payloads. Because of the layered encryption, each server can only decrypt the first remaining payload in the Onion, which reveals the payload intended for that server, while leaving the other payloads still encrypted.
+
+3. **Payload extraction:** After decryption, the server extracts its fee, excess, and the next server's ephemeral public key from its payload. If the server is the last one in the chain, it also receives a rangeproof.
+
+4. **Commitment recalculation:** Using the excess and fee from the decrypted payload, the server recalculates the commitment. The new commitment is `C' = (C - fee*H + excess*G)`, where `C` is the previous commitment, `H` is the hash of a known value and `G` is the generator of the elliptic curve group.
+
+5. **New Onion creation:** The server creates a new Onion with the recalculated commitment, the next server's ephemeral public key, and the remaining still-encrypted payloads (minus the server's own decrypted payload).
+
+This process repeats until the Onion reaches the final server in the path. The final server, after peeling its layer, should find the commitment matches the provided rangeproof, thus ensuring the integrity of the transaction and the anonymity of the involved parties.
+
+### 2-Hop Example
+
+Initial output:
+
+```json
+{
+ "value": 1000,
+ "blind": "c2df4d2331659e8e9c780d27309dba453e34ef48f6e38aab1be50545a0431f95",
+ "commit": "0899dadc2b75d66d738b7dbfcba4a37460622dcedaf222e688a2a84826eaa1cff1"
+}
+
+```
+
+Server keys:
+
+```json
+[
+ {
+ "sk": "a129111d283b13bf93957c06bf6605c3417b4b89db4b5cb2e7dab2c15e36e0a4",
+ "pk": "96ced236bdf1aca722ef68b818445755e6ed4bacf23e19d7b71c43efc5f0077b"
+ },
+ {
+ "sk": "2231414c56488b3596bb56b555ce1b4f8f6ed6b128914760ff89cd42c3d38ad6",
+ "pk": "a2fa3c7043e5080429bdcfb48fb6a8502bca77139d88c6603c9a75234fd6c718"
+ }
+]
+```
+
+For the first hop, the user provides:
+```json
+{
+ "server_pubkey": "96ced236bdf1aca722ef68b818445755e6ed4bacf23e19d7b71c43efc5f0077b",
+ "excess": "a9f15dc4760a1a280f68c6fc16d8aeada415fd66d5da805ff05cac6857a09db4",
+ "fee": 5,
+ "rangeproof": "None"
+}
+```
+The ephemeral key is randomly generated as
+```json
+{
+ "sk": "e8debf70567d3240f5d8e7743e3d986962de4efdd8e638e9989a3afbbafaa85f",
+ "pk": "808ed260a56fe8910444dce931e2d67be0d2c6518134643450d2b9db9dfe7c26"
+}
+```
+
+For the second hop, the user provides:
+```json
+{
+ "server_pubkey": "a2fa3c7043e5080429bdcfb48fb6a8502bca77139d88c6603c9a75234fd6c718",
+ "excess": "d777cf064daf8929e66d2dfc6898fd0cf0774d8546bccb40699c8c47da215663",
+ "fee": 5,
+ "rangeproof": "b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa"
+}
+```
+
+The generated onion becomes:
+```json
+{
+ "commit": "0899dadc2b75d66d738b7dbfcba4a37460622dcedaf222e688a2a84826eaa1cff1",
+ "data": [
+ "d19f914e7a7b5ba1c0ab36d982bd0bc59aa651458a6ee86c2015b57d96424da4a41559069be5ba59e0d2b688f95f1dfb20648e21f9b01ac400cb4879f2ba434c8eb0337d3f58e9e643aa",
+ "a5663659b17f48fba8a335e774f9968e7039db4ce67f3a9d11419e3cf24d96b1c1338d574ba8c6ac36e74b08aef0e61f0dacb5bb769aa76227f894cc49e5ac0f55800130f2a49509123963734ddf21db0fa4f662a3d37c7a0850f8c0c0dd31b26be9cce1e264d60b2240e52f465c7c2b14b11b77fbe3b2fa9bbe78e71df4083d554aa80bcfb1266bd2132ea5c5b356d54a166c16e38c66a58c19c85bb72d11e2097ab2cb13efb4f1de62b3fdf5ee90093de04e8e31e5ef036e155c3db4d7912240abd8807d26f6e9e116bfe90958a8f4347377ed774b1408adb217847efc9dc1bd0067d283a180757f5d23bcdb9e2a87dba903191ecf13ec9ff2e9bed2e774f68fa06f6f51b8f90895058ad3c699d44b9917d2b4b56096bd7885ca8d44d4b635bd975db07780006000faa40b59280aef2bf99088677f0d24efdddd670b7d0b713a9bec34f3c47cc74f594675174508061c7957fcbcf4a5f27ae9fc92f1e9d56f64a3cbcb1492c6845fd08ea04990234ea3cebf62c17f79d3a93fc6ab076fb02563579c903673759b89cfaffef68bf86daf7cb42939ee7fbc8a92e832fb0d7f8071d46323c95676d7e7821c40595d2db8dd7e29bf988eaca8f4ef6ed71a4084a01beea45497d0a5e06476c7092d7774fb4c6f9c52a0ab8bd4cc0d1569696f58deb521c2a11b774f4934fb171f3c2cf0cbfadb02c32a93a70895c5388f04824c486b5075cadf143594f46cb792145932d6a67845b5b744451517728df77f194fd5cbfda7dab160c329d7d340a2cbe3cd2accdecbd32494f75aed1892d65248124aaf9c82951edf49e46de1d80fa465ee70552d76b4f5e5f68d8bbac534f98454adc396050c9eaa7a782c3a27d6f2116a831e75cd1726b8b543738a084d7c1ee592aad80798461eb7a88ba5ea3ecf1a3329ffb7bdc882644efea1d97ceb10206356678e05aa555dd090a695da43e193ecaa239116ba1df350a86a508feb4e57696ec66f17864c394c06de614fe35c7417d51be837dfd2a1eabe135bb985be8d11847990dff17ba3f7b74a68cdfad6d83fec0700fc5fd5"
+ ],
+ "pubkey": "808ed260a56fe8910444dce931e2d67be0d2c6518134643450d2b9db9dfe7c26"
+}
+```
+
+This is provided to the swap (first) server, which uses its server secret key to peel one layer, resulting in:
+```json
+{
+ "payload": {
+ "next_ephemeral_pk": "5353ed848b8b2514aa08c8d9a5109ca4ddafe575c07a2a7cb2f19defa58d8442",
+ "excess": "a9f15dc4760a1a280f68c6fc16d8aeada415fd66d5da805ff05cac6857a09db4",
+ "fee": 5,
+ "proof": "None"
+ },
+ "onion": {
+ "commit":"08b045d9f160fd2528feb50e134a0873ae91a5ab7c44eb2a73ae246eee426bdbde",
+ "data":[
+ "1df9a17573e657575b3fe17a458adc83907a9c643f1121fc5e93ca06467adc1c26fab89974d4b906683625e78ea8b778d6f48628220515699e921e8ec8059f09f4bc81cf84ace0e01fe542b9181e3cb79e4242845cf2b8c0d9a23654bede0cd149e92a6be92fa039602f5e81be2bdaaf7580e1102dd7dfec3df9dbfd4cd6977321bb7212b60810c337c1f83cce1fe9d8a1d8780cbc650dd77082b427e21cae914745f3563557f21315ed16332d09bdeee1cd5b1981433449533e515bfa223202fe8343989f57b9dc783e99b03750be23fa3ba87d973b907b173fb8d8c0790e3db3689f560eaf95c7073e9b71c453261c2e5598cdfed2503200b527724cb1a7dec033bf48e220f1a46b8cf3dc6c961d0173d10b487154fefd850c4e04923ce924a743a8ff403699ca319756e09106d5af5005e214bb02d23d9df99d6ee01fc578ecd82334d3bfdb18eeb3592d98d66a232bc1eecee972cbdf7e2f9dbc60b8fed1767afbb94220efed6a7f7ad51bfd10bc81089e650ca175c0ff0c4b0f5d592fa3166a2689871a89665c17d94a2ce9a4d25f4d174befc0a4b66e72ae6b559ae5250c23806d002a51713800190c25e310aae57167803de7413783f607b8d6cfbb3071dcb6fec6a2bfacd7b0656e8e24060c1a20c9b201ab5d5875455098770d0c4d48ceac73c9d5d6d357fb8fe24d9de27f9bd461e7076c7a28dd1e961d1e373e6890b8d4cf697a8bc11c5b252370ce2be403306390c1bf0d2aaeb6ef5f62064fb87e7fccca5e8a503c8d35651a4fbcfce89e44bd8595dac54e45d11861ca075af49cfdd1dd0bc56085548c8605c6b1706cdbcdfca0d37a77732039cfb9f28b4e216d3cc996d0e69b184d33c54d162d63efa0d7d2738dbcd09690d99277be25ce758d3a90880565d3a03e7c6308a8eb0fbbb450259bf916e1802c72f1226ccd1444503a8ce95a4e296eec4ffbba47e6a41d94b5672499b98f77e72cbed7660e2a0d66598ccc81de1055130393158d4a04805797444b0d8cc713184120a554a130ed4179e51f98db094fab5b1e27accd4b3d2351ebad62"
+ ],
+ "pubkey":"5353ed848b8b2514aa08c8d9a5109ca4ddafe575c07a2a7cb2f19defa58d8442"
+ }
+}
+```
+
+This is passed to the second server, which uses its server secret key to peel the last layer, resulting in:
+```json
+{
+ "payload": {
+ "next_ephemeral_pk": "0000000000000000000000000000000000000000000000000000000000000000",
+ "excess": "d777cf064daf8929e66d2dfc6898fd0cf0774d8546bccb40699c8c47da215663",
+ "fee": 5,
+ "proof": "b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa"
+ },
+ "onion": {
+ "commit": "0996a01db5f4d43b7c185491db087fa0c01dd8e3517a0751787f244ef6c0a0a7f0",
+ "data": [],
+ "pubkey": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+}
+```
+
+The final commitment is returned as part of the last onion packet: `0996a01db5f4d43b7c185491db087fa0c01dd8e3517a0751787f244ef6c0a0a7f0`
+
+The final payload contains a valid rangeproof for it: `b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa`
+
+## Security Considerations
+
+The security of this scheme comes from the use of ephemeral keys and the double encryption of payloads. Each server only has the keys to decrypt its own layer, and cannot derive the keys for any other layers.
+
+This means that a server can only see the data intended for it, and has no information about the rest of the route or the details of any previous hops. This provides strong privacy guarantees for the sender of the transaction.
\ No newline at end of file
diff --git a/doc/store.md b/doc/store.md
new file mode 100644
index 0000000..121354d
--- /dev/null
+++ b/doc/store.md
@@ -0,0 +1,27 @@
+# SwapStore
+
+## Overview
+
+The `SwapStore` is an lmdb database, responsible for storing unprocessed and in-process `SwapData` entries.
+
+The `SwapStore` is used to hold onto new `SwapData` entries until the next swap round, when the mixing process actually occurs. At that time, they will be marked as `InProcess` until the swap is in a confirmed transaction, at which time they will be marked `Completed` and eventually erased.
+
+## Data Model
+
+`SwapData` entries are keyed with prefix 'S' followed by the commitment of the output being swapped. Entries are all unique by key.
+
+### `SwapData`
+
+The `SwapData` structure contains information needed to swap a single output. It has the following fields:
+
+- `excess`: The total excess for the output commitment.
+- `output_commit`: The derived output commitment after applying excess and fee.
+- `rangeproof`: The rangeproof, included only for the final hop (node N).
+- `input`: The transaction input being spent.
+- `fee`: The transaction fee.
+- `onion`: The remaining onion after peeling off our layer.
+- `status`: The status of the swap, represented by the `SwapStatus` enum, which can be one of the following:
+ - `Unprocessed`: The swap has been received but not yet processed.
+ - `InProcess { kernel_hash: Hash }`: The swap is currently being processed, and is expected to be a transaction with the kernel matching the given `kernel_hash`.
+ - `Completed { kernel_hash: Hash, block_hash: Hash }`: The swap has been successfully processed and included in the block matching the given `block_hash`.
+ - `Failed`: The swap has failed, potentially due to expiration or because the output is no longer in the UTXO set.
\ No newline at end of file
diff --git a/doc/swap_api.md b/doc/swap_api.md
new file mode 100644
index 0000000..a779d63
--- /dev/null
+++ b/doc/swap_api.md
@@ -0,0 +1,72 @@
+# Swap Server API
+
+## Overview
+
+The Swap Server provides a single JSON-RPC API with the method `swap`. This API is used by clients to initiate the mixing process for their outputs, obscuring their coin history in a transaction with other users.
+
+## SWAP
+
+### Request
+The `swap` method accepts a single JSON object containing the following fields:
+
+- `onion`: an `Onion` data structure, which is the encrypted onion packet containing the key information necessary to transform the user's output.
+- `comsig`: a Commitment Signature that proves the client knows the secret key and value of the output's commitment.
+
+#### `Onion` data structure
+
+The `Onion` data structure consists of the following fields:
+
+- `pubkey`: an ephemeral pubkey to as the onion originator's portion of the shared secret, represented as an `x25519_dalek::PublicKey`.
+- `commit`: the Pedersen commitment before adjusting the excess and subtracting the fee, represented as a 33-byte `secp256k1` Pedersen commitment.
+- `data`: a vector of encrypted payloads, each representing a layer of the onion. When completely decrypted, these are serialized `Payload` objects.
+
+Each entry in the `enc_payloads` vector corresponds to a server in the system, in order, with the first entry containing the payload for the swap server, and the last entry containing the payload for the final mix server.
+
+#### `Payload` data structure
+
+A `Payload` represents a single, decrypted/peeled layer of an Onion. It consists of the following fields:
+
+- `next_ephemeral_pk`: an `xPublicKey` representing the public key for the next layer.
+- `excess`: a `SecretKey` representing the excess value.
+- `fee`: a `FeeFields` value representing the transaction fee.
+- `rangeproof`: an optional `RangeProof` value.
+
+### Response
+
+A successful call to the 'swap' API will result in an empty JSON-RPC response with no error.
+
+In case of errors, the API will return a `SwapError` type with one of the following variants:
+
+- `InvalidPayloadLength`: The provided number of payloads is invalid.
+- `InvalidComSignature`: The Commitment Signature is invalid.
+- `InvalidRangeproof`: The provided rangeproof is invalid.
+- `MissingRangeproof`: A rangeproof is required but was not supplied.
+- `CoinNotFound`: The output does not exist, or it is already spent.
+- `AlreadySwapped`: The output is already in the swap list.
+- `PeelOnionFailure`: Failed to peel onion layer due to an `OnionError`.
+- `FeeTooLow`: The provided fee is too low.
+- `StoreError`: An error occurred when saving swap to the data store.
+- `ClientError`: An error occurred during client communication.
+- `UnknownError`: An unknown error occurred.
+
+### Example
+
+Here is an example of how to call the 'swap' API:
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "swap",
+ "params": {
+ "comsig": "09ca34db2ac772a9a0e954b4ae2180ba936d8f96219824fe7ec1f5439bef3a0afe7e18867db3d391f37260285feea38ff740b0b49196a4b0a7910c1a72ceca1c5a3e4a53d6e06ffb0536f0dad78812a72ef14e6ff83df8d0dd2aa71615fb00fbe2",
+ "onion": {
+ "commit": "0962da257e8c663d1a35128cf87363657ae6ec4a3c78fda4742a77e9c4f17e1a20",
+ "data": [
+ "fd06dd3e506b1c1e76fd6546beec1e88bb13e7e13be7c02a7e525cd22c43d5dc7a906c77e5c07b08d7a5eeb7e7983b87376b02a33f7582ffc1bf2adac498fefbc2dba840d76d4c8e945f",
+ "ecead273b9b707d101aae71c2c7cb8ce3e7c95347aa730015af206baaf37302df48e5e635ecc94ddf3eee12b314e276f23e29e7dde9f30f712b14ea227801719ecdd1a53999f854a7f4878b905c94905d5f1bfbb4ad9bcf01afeb55070ebcc665d29b0a85093b4d134a52adc76293ad9e963a9f7156dcfc95c1c600a31b919495bf6d3b7ec75eeffcc70aef15b98c43c41468f34b1a96c49b9e20328849a3b12c84d97893145a65d820c37dae51eba62121d681543d060d600167ede3a8c6e807a5765c5ebb2d568366c89bba2b08590a4615822ca64fb848e54267b18fc35fb0f9f6834f1524d7e0da89163e5385de65613e09fed6fec8d9cc60354baa86131b80aa1c8cd5be916a3d757cd8e8253c17158555539a2f8e4d9d1a4b996b218b1af3e7b28bdf9e0f3db2ea9f4d5e11d798d9b7698d037e69df3ca89c2165760963a4d80207917a70a4986d7df83b463547f4d704d28b1eec2e5a93aa70b5b7c73559120e23cd4cfbf76e4d2b21ef215d4c0210001c17318eba633a3c177c18ef88b6c1718e11c552cc77b297dab5c1020557915853434b8ca5698685b3a66bba73164e83d2440473ebb0591df593e0264b605dc3b35055a7de0d40c5c7cc7542dcbe5ade436098dd41e1ac395d2d0baf5c82fdd5932b2e182f8f11a67bccc90e6e63ec8928bd7f0306c6949122fadf12493a7de17f7bfad72501f4f792fca388b3614d6eb3165d948d7c9efe168b5273b132fa27ea6e8df63d70d8b099a9220903b02898b5cc925010ebfab78ccceb19a9f2f6d6e0392c4837977bf0e3e014913e154913c0204913514684f64d7166b3a7203cbab9dddd96ed7db35b4a17fec50abd752348cdf53181ddd6954bc1fb907ed86206dcf05c04efb432cb6ba6db25082b4ce0bf520e3c508163b44c82efaa44b2ec904ddd938a0b99044666941bc72be58e22122027c2fcbc4299e52bc29916eb51206c41e618bce1a5c0d859d116807217282d0883fdabe6f9250cda63082f71fbf921b65ab17cd9bfb0561c4cabe1369c7d6a85c51c0e4f43f51622e70ab4eb0e3fab5"
+ ],
+ "pubkey": "500b161d3bbd9249161d9760ba038d9805be86c0e5273782303a67cda50edb5a"
+ }
+ },
+ "id": "1"
+}
+```
\ No newline at end of file
diff --git a/mwixnet.yml b/mwixnet.yml
index bd1c5d8..b3b3091 100644
--- a/mwixnet.yml
+++ b/mwixnet.yml
@@ -38,6 +38,18 @@ args:
help: Address to bind the rpc server to (e.g. 127.0.0.1:3000)
long: bind_addr
takes_value: true
+ - socks_addr:
+ help: Address to bind the SOCKS5 tor proxy to (e.g. 127.0.0.1:3001)
+ long: socks_addr
+ takes_value: true
+ - prev_server:
+ help: Hex public key of the previous swap/mix server
+ long: prev_server
+ takes_value: true
+ - next_server:
+ help: Hex public key of the next mix server
+ long: next_server
+ takes_value: true
subcommands:
- init-config:
about: Writes a new configuration file
\ No newline at end of file
diff --git a/onion/Cargo.toml b/onion/Cargo.toml
new file mode 100644
index 0000000..43a9e4b
--- /dev/null
+++ b/onion/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "grin_onion"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+blake2 = { package = "blake2-rfc", version = "0.2"}
+byteorder = "1"
+bytes = "0.5.6"
+chacha20 = "0.8.1"
+curve25519-dalek = "2.1"
+ed25519-dalek = "1.0.1"
+hmac = { version = "0.12.0", features = ["std"]}
+itertools = { version = "0.10.3"}
+lazy_static = "1"
+rand = "0.7.3"
+rpassword = "4.0"
+serde = { version = "1", features= ["derive"]}
+serde_derive = "1"
+serde_json = "1"
+sha2 = "0.10.0"
+thiserror = "1.0.31"
+toml = "0.5"
+x25519-dalek = "0.6.0"
+grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
+grin_util = "5"
+grin_api = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
+grin_core = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
+grin_chain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
+grin_keychain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
+grin_servers = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
+grin_store = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
+grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
+grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
+grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
+grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
\ No newline at end of file
diff --git a/src/secp.rs b/onion/src/crypto/comsig.rs
similarity index 61%
rename from src/secp.rs
rename to onion/src/crypto/comsig.rs
index 72fd14d..dd8a0c5 100644
--- a/src/secp.rs
+++ b/onion/src/crypto/comsig.rs
@@ -1,275 +1,189 @@
-pub use secp256k1zkp::aggsig;
-pub use secp256k1zkp::constants::{
- AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE,
- SECRET_KEY_SIZE,
-};
-pub use secp256k1zkp::ecdh::SharedSecret;
-pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY};
-pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
-pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
-
-use blake2::blake2b::Blake2b;
-use byteorder::{BigEndian, ByteOrder};
-use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
-use secp256k1zkp::rand::thread_rng;
-use thiserror::Error;
-
-/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
-#[derive(Clone)]
-pub struct ComSignature {
- pub_nonce: Commitment,
- s: SecretKey,
- t: SecretKey,
-}
-
-/// Error types for Commitment Signatures
-#[derive(Error, Debug)]
-pub enum ComSigError {
- #[error("Commitment signature is invalid")]
- InvalidSig,
- #[error("Secp256k1zkp error: {0:?}")]
- Secp256k1zkp(secp256k1zkp::Error),
-}
-
-impl From for ComSigError {
- fn from(err: secp256k1zkp::Error) -> ComSigError {
- ComSigError::Secp256k1zkp(err)
- }
-}
-
-impl ComSignature {
- pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
- ComSignature {
- pub_nonce: pub_nonce.to_owned(),
- s: s.to_owned(),
- t: t.to_owned(),
- }
- }
-
- #[allow(dead_code)]
- pub fn sign(
- amount: u64,
- blind: &SecretKey,
- msg: &Vec,
- ) -> Result {
- let secp = Secp256k1::with_caps(ContextFlag::Commit);
-
- let mut amt_bytes = [0; 32];
- BigEndian::write_u64(&mut amt_bytes[24..32], amount);
- let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
-
- let k_1 = SecretKey::new(&secp, &mut thread_rng());
- let k_2 = SecretKey::new(&secp, &mut thread_rng());
-
- let commitment = secp.commit(amount, blind.clone())?;
- let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
-
- let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
-
- // s = k_1 + (e * amount)
- let mut s = k_amt.clone();
- s.mul_assign(&secp, &e)?;
- s.add_assign(&secp, &k_1)?;
-
- // t = k_2 + (e * blind)
- let mut t = blind.clone();
- t.mul_assign(&secp, &e)?;
- t.add_assign(&secp, &k_2)?;
-
- Ok(ComSignature::new(&nonce_commitment, &s, &t))
- }
-
- #[allow(non_snake_case)]
- pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<(), ComSigError> {
- let secp = Secp256k1::with_caps(ContextFlag::Commit);
-
- let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?;
-
- let mut Ce = commit.to_pubkey(&secp)?;
- let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?;
- Ce.mul_assign(&secp, &e)?;
-
- let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()];
- let S2 = secp.commit_sum(commits, Vec::new())?;
-
- if S1 != S2 {
- return Err(ComSigError::InvalidSig);
- }
-
- Ok(())
- }
-
- fn calc_challenge(
- secp: &Secp256k1,
- commit: &Commitment,
- nonce_commit: &Commitment,
- msg: &Vec,
- ) -> Result {
- let mut challenge_hasher = Blake2b::new(32);
- challenge_hasher.update(&commit.0);
- challenge_hasher.update(&nonce_commit.0);
- challenge_hasher.update(msg);
-
- let mut challenge = [0; 32];
- challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
-
- Ok(SecretKey::from_slice(&secp, &challenge)?)
- }
-}
-
-/// Serializes a ComSignature to and from hex
-pub mod comsig_serde {
- use super::ComSignature;
- use grin_core::ser::{self, ProtocolVersion};
- use grin_util::ToHex;
- use serde::{Deserialize, Serializer};
-
- /// Serializes a ComSignature as a hex string
- pub fn serialize(comsig: &ComSignature, serializer: S) -> Result
- where
- S: Serializer,
- {
- use serde::ser::Error;
- let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?;
- serializer.serialize_str(&bytes.to_hex())
- }
-
- /// Creates a ComSignature from a hex string
- pub fn deserialize<'de, D>(deserializer: D) -> Result
- where
- D: serde::Deserializer<'de>,
- {
- use serde::de::Error;
- let bytes = String::deserialize(deserializer)
- .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
- let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
- Ok(sig)
- }
-}
-
-#[allow(non_snake_case)]
-impl Readable for ComSignature {
- fn read(reader: &mut R) -> Result {
- let R = Commitment::read(reader)?;
- let s = read_secret_key(reader)?;
- let t = read_secret_key(reader)?;
- Ok(ComSignature::new(&R, &s, &t))
- }
-}
-
-impl Writeable for ComSignature {
- fn write(&self, writer: &mut W) -> Result<(), ser::Error> {
- writer.write_fixed_bytes(self.pub_nonce.0)?;
- writer.write_fixed_bytes(self.s.0)?;
- writer.write_fixed_bytes(self.t.0)?;
- Ok(())
- }
-}
-
-/// Generate a random SecretKey.
-pub fn random_secret() -> SecretKey {
- let secp = Secp256k1::new();
- SecretKey::new(&secp, &mut thread_rng())
-}
-
-/// Deserialize a SecretKey from a Reader
-pub fn read_secret_key(reader: &mut R) -> Result {
- let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?;
- let secp = Secp256k1::with_caps(ContextFlag::None);
- let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?;
- Ok(pk)
-}
-
-/// Build a Pedersen Commitment using the provided value and blinding factor
-pub fn commit(value: u64, blind: &SecretKey) -> Result {
- let secp = Secp256k1::with_caps(ContextFlag::Commit);
- let commit = secp.commit(value, blind.clone())?;
- Ok(commit)
-}
-
-/// Add a blinding factor to an existing Commitment
-pub fn add_excess(
- commitment: &Commitment,
- excess: &SecretKey,
-) -> Result {
- let secp = Secp256k1::with_caps(ContextFlag::Commit);
- let excess_commit: Commitment = secp.commit(0, excess.clone())?;
-
- let commits = vec![commitment.clone(), excess_commit.clone()];
- let sum = secp.commit_sum(commits, Vec::new())?;
- Ok(sum)
-}
-
-/// Subtracts a value (v*H) from an existing commitment
-pub fn sub_value(commitment: &Commitment, value: u64) -> Result {
- let secp = Secp256k1::with_caps(ContextFlag::Commit);
- let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?;
- let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?;
- Ok(sum)
-}
-
-/// Signs the message with the provided SecretKey
-pub fn sign(sk: &SecretKey, msg: &Message) -> Result {
- let secp = Secp256k1::with_caps(ContextFlag::Full);
- let pubkey = PublicKey::from_secret_key(&secp, &sk)?;
- let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?;
- Ok(sig)
-}
-
-#[cfg(test)]
-pub mod test_util {
- use crate::secp::{self, Commitment, RangeProof, Secp256k1};
- use grin_core::core::hash::Hash;
- use grin_util::ToHex;
- use rand::RngCore;
-
- pub fn rand_commit() -> Commitment {
- secp::commit(rand::thread_rng().next_u64(), &secp::random_secret()).unwrap()
- }
-
- pub fn rand_hash() -> Hash {
- Hash::from_hex(secp::random_secret().to_hex().as_str()).unwrap()
- }
-
- pub fn rand_proof() -> RangeProof {
- let secp = Secp256k1::new();
- secp.bullet_proof(
- rand::thread_rng().next_u64(),
- secp::random_secret(),
- secp::random_secret(),
- secp::random_secret(),
- None,
- None,
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey};
-
- use rand::Rng;
- use secp256k1zkp::rand::{thread_rng, RngCore};
-
- /// Test signing and verification of ComSignatures
- #[test]
- fn verify_comsig() -> Result<(), ComSigError> {
- let secp = Secp256k1::with_caps(ContextFlag::Commit);
-
- let amount = thread_rng().next_u64();
- let blind = SecretKey::new(&secp, &mut thread_rng());
- let msg: [u8; 16] = rand::thread_rng().gen();
- let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?;
-
- let commit = secp.commit(amount, blind.clone())?;
- assert!(comsig.verify(&commit, &msg.to_vec()).is_ok());
-
- let wrong_msg: [u8; 16] = rand::thread_rng().gen();
- assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err());
-
- let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?;
- assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err());
-
- Ok(())
- }
-}
+use crate::crypto::secp::{self, Commitment, ContextFlag, Secp256k1, SecretKey};
+
+use blake2::blake2b::Blake2b;
+use byteorder::{BigEndian, ByteOrder};
+use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
+use secp256k1zkp::rand::thread_rng;
+use thiserror::Error;
+
+/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
+#[derive(Clone, Debug)]
+pub struct ComSignature {
+ pub_nonce: Commitment,
+ s: SecretKey,
+ t: SecretKey,
+}
+
+/// Error types for Commitment Signatures
+#[derive(Error, Debug)]
+pub enum ComSigError {
+ #[error("Commitment signature is invalid")]
+ InvalidSig,
+ #[error("Secp256k1zkp error: {0:?}")]
+ Secp256k1zkp(secp256k1zkp::Error),
+}
+
+impl From for ComSigError {
+ fn from(err: secp256k1zkp::Error) -> ComSigError {
+ ComSigError::Secp256k1zkp(err)
+ }
+}
+
+impl ComSignature {
+ pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
+ ComSignature {
+ pub_nonce: pub_nonce.to_owned(),
+ s: s.to_owned(),
+ t: t.to_owned(),
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn sign(
+ amount: u64,
+ blind: &SecretKey,
+ msg: &Vec,
+ ) -> Result {
+ let secp = Secp256k1::with_caps(ContextFlag::Commit);
+
+ let mut amt_bytes = [0; 32];
+ BigEndian::write_u64(&mut amt_bytes[24..32], amount);
+ let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
+
+ let k_1 = SecretKey::new(&secp, &mut thread_rng());
+ let k_2 = SecretKey::new(&secp, &mut thread_rng());
+
+ let commitment = secp.commit(amount, blind.clone())?;
+ let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
+
+ let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
+
+ // s = k_1 + (e * amount)
+ let mut s = k_amt.clone();
+ s.mul_assign(&secp, &e)?;
+ s.add_assign(&secp, &k_1)?;
+
+ // t = k_2 + (e * blind)
+ let mut t = blind.clone();
+ t.mul_assign(&secp, &e)?;
+ t.add_assign(&secp, &k_2)?;
+
+ Ok(ComSignature::new(&nonce_commitment, &s, &t))
+ }
+
+ #[allow(non_snake_case)]
+ pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<(), ComSigError> {
+ let secp = Secp256k1::with_caps(ContextFlag::Commit);
+
+ let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?;
+
+ let mut Ce = commit.to_pubkey(&secp)?;
+ let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?;
+ Ce.mul_assign(&secp, &e)?;
+
+ let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()];
+ let S2 = secp.commit_sum(commits, Vec::new())?;
+
+ if S1 != S2 {
+ return Err(ComSigError::InvalidSig);
+ }
+
+ Ok(())
+ }
+
+ fn calc_challenge(
+ secp: &Secp256k1,
+ commit: &Commitment,
+ nonce_commit: &Commitment,
+ msg: &Vec,
+ ) -> Result {
+ let mut challenge_hasher = Blake2b::new(32);
+ challenge_hasher.update(&commit.0);
+ challenge_hasher.update(&nonce_commit.0);
+ challenge_hasher.update(msg);
+
+ let mut challenge = [0; 32];
+ challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
+
+ Ok(SecretKey::from_slice(&secp, &challenge)?)
+ }
+}
+
+/// Serializes a ComSignature to and from hex
+pub mod comsig_serde {
+ use super::ComSignature;
+ use grin_core::ser::{self, ProtocolVersion};
+ use grin_util::ToHex;
+ use serde::{Deserialize, Serializer};
+
+ /// Serializes a ComSignature as a hex string
+ pub fn serialize(comsig: &ComSignature, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ use serde::ser::Error;
+ let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?;
+ serializer.serialize_str(&bytes.to_hex())
+ }
+
+ /// Creates a ComSignature from a hex string
+ pub fn deserialize<'de, D>(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ use serde::de::Error;
+ let bytes = String::deserialize(deserializer)
+ .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
+ let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
+ Ok(sig)
+ }
+}
+
+#[allow(non_snake_case)]
+impl Readable for ComSignature {
+ fn read(reader: &mut R) -> Result {
+ let R = Commitment::read(reader)?;
+ let s = secp::read_secret_key(reader)?;
+ let t = secp::read_secret_key(reader)?;
+ Ok(ComSignature::new(&R, &s, &t))
+ }
+}
+
+impl Writeable for ComSignature {
+ fn write(&self, writer: &mut W) -> Result<(), ser::Error> {
+ writer.write_fixed_bytes(self.pub_nonce.0)?;
+ writer.write_fixed_bytes(self.s.0)?;
+ writer.write_fixed_bytes(self.t.0)?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey};
+
+ use rand::Rng;
+ use secp256k1zkp::rand::{thread_rng, RngCore};
+
+ /// Test signing and verification of ComSignatures
+ #[test]
+ fn verify_comsig() -> Result<(), ComSigError> {
+ let secp = Secp256k1::with_caps(ContextFlag::Commit);
+
+ let amount = thread_rng().next_u64();
+ let blind = SecretKey::new(&secp, &mut thread_rng());
+ let msg: [u8; 16] = rand::thread_rng().gen();
+ let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?;
+
+ let commit = secp.commit(amount, blind.clone())?;
+ assert!(comsig.verify(&commit, &msg.to_vec()).is_ok());
+
+ let wrong_msg: [u8; 16] = rand::thread_rng().gen();
+ assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err());
+
+ let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?;
+ assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err());
+
+ Ok(())
+ }
+}
diff --git a/onion/src/crypto/dalek.rs b/onion/src/crypto/dalek.rs
new file mode 100644
index 0000000..70c7654
--- /dev/null
+++ b/onion/src/crypto/dalek.rs
@@ -0,0 +1,272 @@
+use crate::crypto::secp::SecretKey;
+
+use ed25519_dalek::{Keypair, PublicKey, Signature, Signer, Verifier};
+use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
+use grin_util::ToHex;
+use thiserror::Error;
+
+/// Error types for Dalek structures and logic
+#[derive(Clone, Error, Debug, PartialEq)]
+pub enum DalekError {
+ #[error("Hex error {0:?}")]
+ HexError(String),
+ #[error("Failed to parse secret key")]
+ KeyParseError,
+ #[error("Failed to verify signature")]
+ SigVerifyFailed,
+}
+
+/// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization
+#[derive(Clone, Debug, PartialEq)]
+pub struct DalekPublicKey(PublicKey);
+
+impl DalekPublicKey {
+ /// Convert DalekPublicKey to hex string
+ pub fn to_hex(&self) -> String {
+ self.0.to_hex()
+ }
+
+ /// Convert hex string to DalekPublicKey.
+ pub fn from_hex(hex: &str) -> Result {
+ let bytes = grin_util::from_hex(hex)
+ .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
+ let pk = PublicKey::from_bytes(bytes.as_ref())
+ .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
+ Ok(DalekPublicKey(pk))
+ }
+
+ /// Compute DalekPublicKey from a SecretKey
+ pub fn from_secret(key: &SecretKey) -> Self {
+ let secret = ed25519_dalek::SecretKey::from_bytes(&key.0).unwrap();
+ let pk: PublicKey = (&secret).into();
+ DalekPublicKey(pk)
+ }
+}
+
+impl AsRef for DalekPublicKey {
+ fn as_ref(&self) -> &PublicKey {
+ &self.0
+ }
+}
+
+/// Serializes an Option to and from hex
+pub mod option_dalek_pubkey_serde {
+ use super::DalekPublicKey;
+ use grin_util::ToHex;
+ use serde::de::Error;
+ use serde::{Deserialize, Deserializer, Serializer};
+
+ ///
+ pub fn serialize(pk: &Option, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ match pk {
+ Some(pk) => serializer.serialize_str(&pk.0.to_hex()),
+ None => serializer.serialize_none(),
+ }
+ }
+
+ ///
+ pub fn deserialize<'de, D>(deserializer: D) -> Result