mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-01-20 19:11:09 +03:00
Merge pull request #20 from scilio/multiserver
Multi-server support & Generate ephemeral pubkeys randomly for each server
This commit is contained in:
commit
19e12a1f2e
29 changed files with 3558 additions and 1429 deletions
133
Cargo.lock
generated
133
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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 (node<sub>i</sub> with i=1...n) are agreed upon in advance. They each have a known public key.
|
||||
A set of n CoinSwap servers (N<sub>i</sub> with i=1...n) are agreed upon in advance. They each have a known public key.
|
||||
|
||||
We refer to the first server (N<sub>1</sub>) as the "Swap Server." This is the server that wallets can submit their coinswaps too.
|
||||
|
||||
We refer to the remaining servers (N<sub>2</sub>...N<sub>n</sub>) 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 (n<sub>1</sub>) provides the `swap` API, publicly available for use by GRIN wallets.
|
||||
The Swap Server (N<sub>1</sub>) provides the `swap` API, which is publicly available for use by GRIN wallets.
|
||||
|
||||
**jsonrpc:** `2.0`
|
||||
**method:** `swap`
|
||||
|
|
202
doc/onion.md
Normal file
202
doc/onion.md
Normal file
|
@ -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<ChaCha20, OnionError> {
|
||||
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.
|
27
doc/store.md
Normal file
27
doc/store.md
Normal file
|
@ -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.
|
72
doc/swap_api.md
Normal file
72
doc/swap_api.md
Normal file
|
@ -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"
|
||||
}
|
||||
```
|
12
mwixnet.yml
12
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
|
38
onion/Cargo.toml
Normal file
38
onion/Cargo.toml
Normal file
|
@ -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" }
|
|
@ -1,12 +1,4 @@
|
|||
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 crate::crypto::secp::{self, Commitment, ContextFlag, Secp256k1, SecretKey};
|
||||
|
||||
use blake2::blake2b::Blake2b;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
|
@ -15,7 +7,7 @@ use secp256k1zkp::rand::thread_rng;
|
|||
use thiserror::Error;
|
||||
|
||||
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ComSignature {
|
||||
pub_nonce: Commitment,
|
||||
s: SecretKey,
|
||||
|
@ -151,8 +143,8 @@ pub mod comsig_serde {
|
|||
impl Readable for ComSignature {
|
||||
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
|
||||
let R = Commitment::read(reader)?;
|
||||
let s = read_secret_key(reader)?;
|
||||
let t = read_secret_key(reader)?;
|
||||
let s = secp::read_secret_key(reader)?;
|
||||
let t = secp::read_secret_key(reader)?;
|
||||
Ok(ComSignature::new(&R, &s, &t))
|
||||
}
|
||||
}
|
||||
|
@ -166,84 +158,6 @@ impl Writeable for ComSignature {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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<R: Reader>(reader: &mut R) -> Result<SecretKey, ser::Error> {
|
||||
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<Commitment, secp256k1zkp::Error> {
|
||||
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<Commitment, secp256k1zkp::Error> {
|
||||
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<Commitment, secp256k1zkp::Error> {
|
||||
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<Signature, secp256k1zkp::Error> {
|
||||
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};
|
272
onion/src/crypto/dalek.rs
Normal file
272
onion/src/crypto/dalek.rs
Normal file
|
@ -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<Self, DalekError> {
|
||||
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<PublicKey> for DalekPublicKey {
|
||||
fn as_ref(&self) -> &PublicKey {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes an Option<DalekPublicKey> 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<S>(pk: &Option<DalekPublicKey>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<Option<DalekPublicKey>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<String>::deserialize(deserializer).and_then(|res| match res {
|
||||
Some(string) => DalekPublicKey::from_hex(&string)
|
||||
.map_err(|e| Error::custom(e.to_string()))
|
||||
.and_then(|pk: DalekPublicKey| Ok(Some(pk))),
|
||||
None => Ok(None),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Readable for DalekPublicKey {
|
||||
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
|
||||
let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?)
|
||||
.map_err(|_| ser::Error::CorruptedData)?;
|
||||
Ok(DalekPublicKey(pk))
|
||||
}
|
||||
}
|
||||
|
||||
impl Writeable for DalekPublicKey {
|
||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_fixed_bytes(self.0.to_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates an ed25519_dalek::Signature and provides (de-)serialization
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DalekSignature(Signature);
|
||||
|
||||
impl DalekSignature {
|
||||
/// Convert hex string to DalekSignature.
|
||||
pub fn from_hex(hex: &str) -> Result<Self, DalekError> {
|
||||
let bytes = grin_util::from_hex(hex)
|
||||
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
|
||||
let sig = Signature::from_bytes(bytes.as_ref())
|
||||
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
|
||||
Ok(DalekSignature(sig))
|
||||
}
|
||||
|
||||
/// Verifies DalekSignature
|
||||
pub fn verify(&self, pk: &DalekPublicKey, msg: &[u8]) -> Result<(), DalekError> {
|
||||
pk.as_ref()
|
||||
.verify(&msg, &self.0)
|
||||
.map_err(|_| DalekError::SigVerifyFailed)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Signature> for DalekSignature {
|
||||
fn as_ref(&self) -> &Signature {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes a DalekSignature to and from hex
|
||||
pub mod dalek_sig_serde {
|
||||
use super::DalekSignature;
|
||||
use grin_util::ToHex;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
///
|
||||
pub fn serialize<S>(sig: &DalekSignature, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&sig.0.to_hex())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<DalekSignature, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let str = String::deserialize(deserializer)?;
|
||||
let sig = DalekSignature::from_hex(&str).map_err(|e| Error::custom(e.to_string()))?;
|
||||
Ok(sig)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign(sk: &SecretKey, message: &[u8]) -> Result<DalekSignature, DalekError> {
|
||||
let secret =
|
||||
ed25519_dalek::SecretKey::from_bytes(&sk.0).map_err(|_| DalekError::KeyParseError)?;
|
||||
let public: PublicKey = (&secret).into();
|
||||
let keypair = Keypair { secret, public };
|
||||
let sig = keypair.sign(&message);
|
||||
Ok(DalekSignature(sig))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::dalek::test_util::rand_keypair;
|
||||
use grin_core::ser::{self, ProtocolVersion};
|
||||
use grin_util::ToHex;
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
struct TestPubKeySerde {
|
||||
#[serde(with = "option_dalek_pubkey_serde", default)]
|
||||
pk: Option<DalekPublicKey>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pubkey_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test from_hex
|
||||
let rand_pk = rand_keypair().1;
|
||||
let pk_from_hex = DalekPublicKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap();
|
||||
assert_eq!(rand_pk.0, pk_from_hex.0);
|
||||
|
||||
// Test ser (de-)serialization
|
||||
let bytes = ser::ser_vec(&rand_pk, ProtocolVersion::local()).unwrap();
|
||||
assert_eq!(bytes.len(), 32);
|
||||
let pk_from_deser: DalekPublicKey = ser::deserialize_default(&mut &bytes[..]).unwrap();
|
||||
assert_eq!(rand_pk.0, pk_from_deser.0);
|
||||
|
||||
// Test serde with Some(rand_pk)
|
||||
let some = TestPubKeySerde {
|
||||
pk: Some(rand_pk.clone()),
|
||||
};
|
||||
let val = serde_json::to_value(some.clone()).unwrap();
|
||||
if let Value::Object(o) = &val {
|
||||
if let Value::String(s) = o.get("pk").unwrap() {
|
||||
assert_eq!(s, &rand_pk.0.to_hex());
|
||||
} else {
|
||||
panic!("Invalid type");
|
||||
}
|
||||
} else {
|
||||
panic!("Invalid type")
|
||||
}
|
||||
assert_eq!(some, serde_json::from_value(val).unwrap());
|
||||
|
||||
// Test serde with empty pk field
|
||||
let none = TestPubKeySerde { pk: None };
|
||||
let val = serde_json::to_value(none.clone()).unwrap();
|
||||
if let Value::Object(o) = &val {
|
||||
if let Value::Null = o.get("pk").unwrap() {
|
||||
// ok
|
||||
} else {
|
||||
panic!("Invalid type");
|
||||
}
|
||||
} else {
|
||||
panic!("Invalid type")
|
||||
}
|
||||
assert_eq!(none, serde_json::from_value(val).unwrap());
|
||||
|
||||
// Test serde with no pk field
|
||||
let none2 = serde_json::from_str::<TestPubKeySerde>("{}").unwrap();
|
||||
assert_eq!(none, none2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
struct TestSigSerde {
|
||||
#[serde(with = "dalek_sig_serde")]
|
||||
sig: DalekSignature,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sig_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Sign a message
|
||||
let (sk, pk) = rand_keypair();
|
||||
let msg: [u8; 16] = rand::thread_rng().gen();
|
||||
let sig = sign(&sk, &msg).unwrap();
|
||||
|
||||
// Verify signature
|
||||
assert!(sig.verify(&pk, &msg).is_ok());
|
||||
|
||||
// Wrong message
|
||||
let wrong_msg: [u8; 16] = rand::thread_rng().gen();
|
||||
assert!(sig.verify(&pk, &wrong_msg).is_err());
|
||||
|
||||
// Wrong pubkey
|
||||
let wrong_pk = rand_keypair().1;
|
||||
assert!(sig.verify(&wrong_pk, &msg).is_err());
|
||||
|
||||
// Test from_hex
|
||||
let sig_from_hex = DalekSignature::from_hex(sig.0.to_hex().as_str()).unwrap();
|
||||
assert_eq!(sig.0, sig_from_hex.0);
|
||||
|
||||
// Test serde (de-)serialization
|
||||
let serde_test = TestSigSerde { sig: sig.clone() };
|
||||
let val = serde_json::to_value(serde_test.clone()).unwrap();
|
||||
if let Value::Object(o) = &val {
|
||||
if let Value::String(s) = o.get("sig").unwrap() {
|
||||
assert_eq!(s, &sig.0.to_hex());
|
||||
} else {
|
||||
panic!("Invalid type");
|
||||
}
|
||||
} else {
|
||||
panic!("Invalid type")
|
||||
}
|
||||
assert_eq!(serde_test, serde_json::from_value(val).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
3
onion/src/crypto/mod.rs
Normal file
3
onion/src/crypto/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod comsig;
|
||||
pub mod dalek;
|
||||
pub mod secp;
|
62
onion/src/crypto/secp.rs
Normal file
62
onion/src/crypto/secp.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
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 grin_core::ser::{self, Reader};
|
||||
use secp256k1zkp::rand::thread_rng;
|
||||
|
||||
/// 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<R: Reader>(reader: &mut R) -> Result<SecretKey, ser::Error> {
|
||||
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<Commitment, secp256k1zkp::Error> {
|
||||
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<Commitment, secp256k1zkp::Error> {
|
||||
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<Commitment, secp256k1zkp::Error> {
|
||||
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<Signature, secp256k1zkp::Error> {
|
||||
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)
|
||||
}
|
170
onion/src/lib.rs
Normal file
170
onion/src/lib.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
pub mod crypto;
|
||||
pub mod onion;
|
||||
pub mod util;
|
||||
|
||||
use crate::crypto::secp::{random_secret, Commitment, SecretKey};
|
||||
use crate::onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes};
|
||||
|
||||
use chacha20::cipher::StreamCipher;
|
||||
use grin_core::core::FeeFields;
|
||||
use secp256k1zkp::pedersen::RangeProof;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
use x25519_dalek::{SharedSecret, StaticSecret};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Hop {
|
||||
pub server_pubkey: xPublicKey,
|
||||
pub excess: SecretKey,
|
||||
pub fee: FeeFields,
|
||||
pub rangeproof: Option<RangeProof>,
|
||||
}
|
||||
|
||||
pub fn new_hop(
|
||||
server_key: &SecretKey,
|
||||
hop_excess: &SecretKey,
|
||||
fee: u32,
|
||||
proof: Option<RangeProof>,
|
||||
) -> Hop {
|
||||
Hop {
|
||||
server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
|
||||
excess: hop_excess.clone(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: proof,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an Onion for the Commitment, encrypting the payload for each hop
|
||||
pub fn create_onion(commitment: &Commitment, hops: &Vec<Hop>) -> Result<Onion, OnionError> {
|
||||
if hops.is_empty() {
|
||||
return Ok(Onion {
|
||||
ephemeral_pubkey: xPublicKey::from([0u8; 32]),
|
||||
commit: commitment.clone(),
|
||||
enc_payloads: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
let mut shared_secrets: Vec<SharedSecret> = Vec::new();
|
||||
let mut enc_payloads: Vec<RawBytes> = Vec::new();
|
||||
let mut ephemeral_sk = StaticSecret::from(random_secret().0);
|
||||
let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk);
|
||||
for i in 0..hops.len() {
|
||||
let hop = &hops[i];
|
||||
let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey);
|
||||
shared_secrets.push(shared_secret);
|
||||
|
||||
ephemeral_sk = StaticSecret::from(random_secret().0);
|
||||
let next_ephemeral_pk = if i < (hops.len() - 1) {
|
||||
xPublicKey::from(&ephemeral_sk)
|
||||
} else {
|
||||
xPublicKey::from([0u8; 32])
|
||||
};
|
||||
|
||||
let payload = Payload {
|
||||
next_ephemeral_pk,
|
||||
excess: hop.excess.clone(),
|
||||
fee: hop.fee.clone(),
|
||||
rangeproof: hop.rangeproof.clone(),
|
||||
};
|
||||
enc_payloads.push(payload.serialize()?);
|
||||
}
|
||||
|
||||
for i in (0..shared_secrets.len()).rev() {
|
||||
let mut cipher = new_stream_cipher(&shared_secrets[i])?;
|
||||
for j in i..shared_secrets.len() {
|
||||
cipher.apply_keystream(&mut enc_payloads[j]);
|
||||
}
|
||||
}
|
||||
|
||||
let onion = Onion {
|
||||
ephemeral_pubkey: onion_ephemeral_pk,
|
||||
commit: commitment.clone(),
|
||||
enc_payloads,
|
||||
};
|
||||
Ok(onion)
|
||||
}
|
||||
|
||||
pub mod test_util {
|
||||
use super::*;
|
||||
use crate::crypto::dalek::DalekPublicKey;
|
||||
use crate::crypto::secp;
|
||||
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_util::ToHex;
|
||||
use rand::{thread_rng, RngCore};
|
||||
use secp256k1zkp::Secp256k1;
|
||||
|
||||
pub fn rand_onion() -> Onion {
|
||||
let commit = rand_commit();
|
||||
let mut hops = Vec::new();
|
||||
let k = (thread_rng().next_u64() % 5) + 1;
|
||||
for i in 0..k {
|
||||
let rangeproof = if i == (k - 1) {
|
||||
Some(rand_proof())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hop = new_hop(
|
||||
&random_secret(),
|
||||
&random_secret(),
|
||||
thread_rng().next_u32(),
|
||||
rangeproof,
|
||||
);
|
||||
hops.push(hop);
|
||||
}
|
||||
|
||||
create_onion(&commit, &hops).unwrap()
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn proof(
|
||||
value: u64,
|
||||
fee: u32,
|
||||
input_blind: &SecretKey,
|
||||
hop_excesses: &Vec<&SecretKey>,
|
||||
) -> (Commitment, RangeProof) {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let mut blind = input_blind.clone();
|
||||
for hop_excess in hop_excesses {
|
||||
blind.add_assign(&secp, &hop_excess).unwrap();
|
||||
}
|
||||
|
||||
let out_value = value - (fee as u64);
|
||||
|
||||
let rp = secp.bullet_proof(
|
||||
out_value,
|
||||
blind.clone(),
|
||||
secp::random_secret(),
|
||||
secp::random_secret(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
(secp::commit(out_value, &blind).unwrap(), rp)
|
||||
}
|
||||
|
||||
pub fn rand_keypair() -> (SecretKey, DalekPublicKey) {
|
||||
let sk = random_secret();
|
||||
let pk = DalekPublicKey::from_secret(&sk);
|
||||
(sk, pk)
|
||||
}
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
use crate::secp::{self, Commitment, SecretKey};
|
||||
use crate::types::Payload;
|
||||
use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use crate::util::{read_optional, vec_to_array, write_optional};
|
||||
|
||||
use crate::onion::OnionError::{InvalidKeyLength, SerializationError};
|
||||
use chacha20::cipher::{NewCipher, StreamCipher};
|
||||
use chacha20::{ChaCha20, Key, Nonce};
|
||||
use grin_core::core::FeeFields;
|
||||
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
use grin_util::{self, ToHex};
|
||||
use hmac::digest::InvalidLength;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::convert::TryInto;
|
||||
use sha2::Sha256;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::result::Result;
|
||||
|
@ -19,7 +18,9 @@ use thiserror::Error;
|
|||
use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
type RawBytes = Vec<u8>;
|
||||
pub type RawBytes = Vec<u8>;
|
||||
|
||||
const CURRENT_ONION_VERSION: u8 = 0;
|
||||
|
||||
/// A data packet with layers of encryption
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -53,8 +54,67 @@ impl Hash for Onion {
|
|||
}
|
||||
}
|
||||
|
||||
fn vec_to_32_byte_arr(v: Vec<u8>) -> Result<[u8; 32], OnionError> {
|
||||
v.try_into().map_err(|_| InvalidKeyLength)
|
||||
/// A single, decrypted/peeled layer of an Onion.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Payload {
|
||||
pub next_ephemeral_pk: xPublicKey,
|
||||
pub excess: SecretKey,
|
||||
pub fee: FeeFields,
|
||||
pub rangeproof: Option<RangeProof>,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
pub fn deserialize(bytes: &Vec<u8>) -> Result<Payload, ser::Error> {
|
||||
let payload: Payload = ser::deserialize_default(&mut &bytes[..])?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> {
|
||||
let mut vec = vec![];
|
||||
ser::serialize_default(&mut vec, &self)?;
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
impl Readable for Payload {
|
||||
fn read<R: Reader>(reader: &mut R) -> Result<Payload, ser::Error> {
|
||||
let version = reader.read_u8()?;
|
||||
if version != CURRENT_ONION_VERSION {
|
||||
return Err(ser::Error::UnsupportedProtocolVersion);
|
||||
}
|
||||
|
||||
let next_ephemeral_pk =
|
||||
xPublicKey::from(vec_to_array::<32>(&reader.read_fixed_bytes(32)?)?);
|
||||
let excess = secp::read_secret_key(reader)?;
|
||||
let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?;
|
||||
let rangeproof = read_optional(reader)?;
|
||||
Ok(Payload {
|
||||
next_ephemeral_pk,
|
||||
excess,
|
||||
fee,
|
||||
rangeproof,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Writeable for Payload {
|
||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_u8(CURRENT_ONION_VERSION)?;
|
||||
writer.write_fixed_bytes(&self.next_ephemeral_pk.as_bytes())?;
|
||||
writer.write_fixed_bytes(&self.excess)?;
|
||||
writer.write_u64(self.fee.into())?;
|
||||
write_optional(writer, &self.rangeproof)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// An onion with a layer decrypted
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PeeledOnion {
|
||||
/// The payload from the peeled layer
|
||||
pub payload: Payload,
|
||||
/// The onion remaining after a layer was peeled
|
||||
pub onion: Onion,
|
||||
}
|
||||
|
||||
impl Onion {
|
||||
|
@ -65,8 +125,8 @@ impl Onion {
|
|||
}
|
||||
|
||||
/// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload
|
||||
pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<(Payload, Onion), OnionError> {
|
||||
let shared_secret = StaticSecret::from(secret_key.0).diffie_hellman(&self.ephemeral_pubkey);
|
||||
pub fn peel_layer(&self, server_key: &SecretKey) -> Result<PeeledOnion, OnionError> {
|
||||
let shared_secret = StaticSecret::from(server_key.0).diffie_hellman(&self.ephemeral_pubkey);
|
||||
let mut cipher = new_stream_cipher(&shared_secret)?;
|
||||
|
||||
let mut decrypted_bytes = self.enc_payloads[0].clone();
|
||||
|
@ -86,15 +146,6 @@ impl Onion {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?;
|
||||
|
||||
let ephemeral_key = StaticSecret::from(
|
||||
*blinding_factor
|
||||
.diffie_hellman(&self.ephemeral_pubkey)
|
||||
.as_bytes(),
|
||||
);
|
||||
let ephemeral_pubkey = xPublicKey::from(&ephemeral_key);
|
||||
|
||||
let mut commitment = self.commit.clone();
|
||||
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)
|
||||
.map_err(|e| OnionError::CalcCommitError(e))?;
|
||||
|
@ -102,31 +153,18 @@ impl Onion {
|
|||
.map_err(|e| OnionError::CalcCommitError(e))?;
|
||||
|
||||
let peeled_onion = Onion {
|
||||
ephemeral_pubkey,
|
||||
ephemeral_pubkey: decrypted_payload.next_ephemeral_pk,
|
||||
commit: commitment.clone(),
|
||||
enc_payloads,
|
||||
};
|
||||
Ok((decrypted_payload, peeled_onion))
|
||||
Ok(PeeledOnion {
|
||||
payload: decrypted_payload,
|
||||
onion: peeled_onion,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_blinding_factor(
|
||||
shared_secret: &SharedSecret,
|
||||
ephemeral_pubkey: &xPublicKey,
|
||||
) -> Result<StaticSecret, OnionError> {
|
||||
let mut hasher = Sha256::default();
|
||||
hasher.update(ephemeral_pubkey.as_bytes());
|
||||
hasher.update(shared_secret.as_bytes());
|
||||
let hashed: [u8; 32] = hasher
|
||||
.finalize()
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| InvalidKeyLength)?;
|
||||
|
||||
Ok(StaticSecret::from(hashed))
|
||||
}
|
||||
|
||||
fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20, OnionError> {
|
||||
pub fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20, OnionError> {
|
||||
let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?;
|
||||
mu_hmac.update(shared_secret.as_bytes());
|
||||
let mukey = mu_hmac.finalize().into_bytes();
|
||||
|
@ -152,8 +190,7 @@ impl Writeable for Onion {
|
|||
|
||||
impl Readable for Onion {
|
||||
fn read<R: Reader>(reader: &mut R) -> Result<Onion, ser::Error> {
|
||||
let pubkey_bytes: [u8; 32] =
|
||||
vec_to_32_byte_arr(reader.read_fixed_bytes(32)?).map_err(|_| ser::Error::CountError)?;
|
||||
let pubkey_bytes: [u8; 32] = vec_to_array(&reader.read_fixed_bytes(32)?)?;
|
||||
let ephemeral_pubkey = xPublicKey::from(pubkey_bytes);
|
||||
let commit = Commitment::read(reader)?;
|
||||
let mut enc_payloads: Vec<RawBytes> = Vec::new();
|
||||
|
@ -224,9 +261,9 @@ impl<'de> serde::de::Deserialize<'de> for Onion {
|
|||
let vec =
|
||||
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
||||
pubkey =
|
||||
Some(xPublicKey::from(vec_to_32_byte_arr(vec).map_err(|_| {
|
||||
serde::de::Error::custom("Invalid length pubkey")
|
||||
})?));
|
||||
Some(xPublicKey::from(vec_to_array::<32>(&vec).map_err(
|
||||
|_| serde::de::Error::custom("Invalid length pubkey"),
|
||||
)?));
|
||||
}
|
||||
Field::Commit => {
|
||||
let val: String = map.next_value()?;
|
||||
|
@ -279,148 +316,48 @@ pub enum OnionError {
|
|||
|
||||
impl From<InvalidLength> for OnionError {
|
||||
fn from(_err: InvalidLength) -> OnionError {
|
||||
InvalidKeyLength
|
||||
OnionError::InvalidKeyLength
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ser::Error> for OnionError {
|
||||
fn from(err: ser::Error) -> OnionError {
|
||||
SerializationError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use super::{Onion, OnionError, RawBytes};
|
||||
use crate::secp::test_util::{rand_commit, rand_proof};
|
||||
use crate::secp::{random_secret, Commitment, SecretKey};
|
||||
use crate::types::Payload;
|
||||
|
||||
use chacha20::cipher::StreamCipher;
|
||||
use grin_core::core::FeeFields;
|
||||
use rand::{thread_rng, RngCore};
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
use x25519_dalek::{SharedSecret, StaticSecret};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Hop {
|
||||
pub pubkey: xPublicKey,
|
||||
pub payload: Payload,
|
||||
}
|
||||
/*
|
||||
Choose random xi for each node ni and create a Payload (Pi) for each containing xi
|
||||
Build a rangeproof for Cn=Cin+(Σx1...n)*G and include it in payload Pn
|
||||
Choose random initial ephemeral keypair (r1, R1)
|
||||
Derive remaining ephemeral keypairs such that ri+1=ri*Sha256(Ri||si) where si=ECDH(Ri, Ki)
|
||||
For each node ni, use ChaCha20 stream cipher with key=HmacSha256("MWIXNET"||si) and nonce "NONCE1234567" to encrypt payloads Pi...n
|
||||
*/
|
||||
|
||||
/// Create an Onion for the Commitment, encrypting the payload for each hop
|
||||
pub fn create_onion(commitment: &Commitment, hops: &Vec<Hop>) -> Result<Onion, OnionError> {
|
||||
let initial_key = StaticSecret::new(&mut thread_rng());
|
||||
let mut ephemeral_key = initial_key.clone();
|
||||
|
||||
let mut shared_secrets: Vec<SharedSecret> = Vec::new();
|
||||
let mut enc_payloads: Vec<RawBytes> = Vec::new();
|
||||
for hop in hops {
|
||||
let shared_secret = ephemeral_key.diffie_hellman(&hop.pubkey);
|
||||
|
||||
let ephemeral_pubkey = xPublicKey::from(&ephemeral_key);
|
||||
let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?;
|
||||
|
||||
shared_secrets.push(shared_secret);
|
||||
enc_payloads.push(hop.payload.serialize()?);
|
||||
ephemeral_key = StaticSecret::from(
|
||||
*ephemeral_key
|
||||
.diffie_hellman(&xPublicKey::from(&blinding_factor))
|
||||
.as_bytes(),
|
||||
);
|
||||
}
|
||||
|
||||
for i in (0..shared_secrets.len()).rev() {
|
||||
let mut cipher = super::new_stream_cipher(&shared_secrets[i])?;
|
||||
for j in i..shared_secrets.len() {
|
||||
cipher.apply_keystream(&mut enc_payloads[j]);
|
||||
}
|
||||
}
|
||||
|
||||
let onion = Onion {
|
||||
ephemeral_pubkey: xPublicKey::from(&initial_key),
|
||||
commit: commitment.clone(),
|
||||
enc_payloads,
|
||||
};
|
||||
Ok(onion)
|
||||
}
|
||||
|
||||
pub fn rand_onion() -> Onion {
|
||||
let commit = rand_commit();
|
||||
let mut hops = Vec::new();
|
||||
let k = (thread_rng().next_u64() % 5) + 1;
|
||||
for i in 0..k {
|
||||
let hop = Hop {
|
||||
pubkey: xPublicKey::from(random_secret().0),
|
||||
payload: Payload {
|
||||
excess: random_secret(),
|
||||
fee: FeeFields::from(thread_rng().next_u32()),
|
||||
rangeproof: if i == (k - 1) {
|
||||
Some(rand_proof())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
};
|
||||
hops.push(hop);
|
||||
}
|
||||
|
||||
create_onion(&commit, &hops).unwrap()
|
||||
}
|
||||
|
||||
/// Calculates the expected next ephemeral pubkey after peeling a layer off of the Onion.
|
||||
pub fn next_ephemeral_pubkey(
|
||||
onion: &Onion,
|
||||
server_key: &SecretKey,
|
||||
) -> Result<xPublicKey, OnionError> {
|
||||
let shared_secret =
|
||||
StaticSecret::from(server_key.0.clone()).diffie_hellman(&onion.ephemeral_pubkey);
|
||||
let blinding_factor = super::calc_blinding_factor(&shared_secret, &onion.ephemeral_pubkey)?;
|
||||
let mul = blinding_factor.diffie_hellman(&onion.ephemeral_pubkey);
|
||||
Ok(xPublicKey::from(&StaticSecret::from(*mul.as_bytes())))
|
||||
OnionError::SerializationError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::test_util::{self, Hop};
|
||||
use crate::secp;
|
||||
use crate::types::Payload;
|
||||
use super::*;
|
||||
use crate::crypto::secp::random_secret;
|
||||
use crate::{new_hop, Hop};
|
||||
|
||||
use grin_core::core::FeeFields;
|
||||
use x25519_dalek::{PublicKey as xPublicKey, StaticSecret};
|
||||
|
||||
/// Test end-to-end Onion creation and unwrapping logic.
|
||||
#[test]
|
||||
fn onion() {
|
||||
let total_fee: u64 = 10;
|
||||
let fee_per_hop: u64 = 2;
|
||||
let fee_per_hop: u32 = 2;
|
||||
let in_value: u64 = 1000;
|
||||
let out_value: u64 = in_value - total_fee;
|
||||
let blind = secp::random_secret();
|
||||
let blind = random_secret();
|
||||
let commitment = secp::commit(in_value, &blind).unwrap();
|
||||
|
||||
let mut hops: Vec<Hop> = Vec::new();
|
||||
let mut keys: Vec<secp::SecretKey> = Vec::new();
|
||||
let mut keys: Vec<SecretKey> = Vec::new();
|
||||
let mut final_commit = secp::commit(out_value, &blind).unwrap();
|
||||
let mut final_blind = blind.clone();
|
||||
for i in 0..5 {
|
||||
keys.push(secp::random_secret());
|
||||
keys.push(random_secret());
|
||||
|
||||
let excess = secp::random_secret();
|
||||
let excess = random_secret();
|
||||
|
||||
let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
|
||||
final_blind.add_assign(&secp, &excess).unwrap();
|
||||
final_commit = secp::add_excess(&final_commit, &excess).unwrap();
|
||||
let proof = if i == 4 {
|
||||
let n1 = secp::random_secret();
|
||||
let n1 = random_secret();
|
||||
let rp = secp.bullet_proof(
|
||||
out_value,
|
||||
final_blind.clone(),
|
||||
|
@ -435,34 +372,26 @@ pub mod tests {
|
|||
None
|
||||
};
|
||||
|
||||
hops.push(Hop {
|
||||
pubkey: xPublicKey::from(&StaticSecret::from(keys[i].0.clone())),
|
||||
payload: Payload {
|
||||
excess,
|
||||
fee: FeeFields::from(fee_per_hop as u32),
|
||||
rangeproof: proof,
|
||||
},
|
||||
});
|
||||
let hop = new_hop(&keys[i], &excess, fee_per_hop, proof);
|
||||
hops.push(hop);
|
||||
}
|
||||
|
||||
let mut onion_packet = test_util::create_onion(&commitment, &hops).unwrap();
|
||||
|
||||
let mut payload = Payload {
|
||||
excess: secp::random_secret(),
|
||||
next_ephemeral_pk: onion_packet.ephemeral_pubkey.clone(),
|
||||
excess: random_secret(),
|
||||
fee: FeeFields::from(fee_per_hop as u32),
|
||||
rangeproof: None,
|
||||
};
|
||||
for i in 0..5 {
|
||||
let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
|
||||
payload = peeled.0;
|
||||
onion_packet = peeled.1;
|
||||
payload = peeled.payload;
|
||||
onion_packet = peeled.onion;
|
||||
}
|
||||
|
||||
assert!(payload.rangeproof.is_some());
|
||||
assert_eq!(
|
||||
payload.rangeproof.unwrap(),
|
||||
hops[4].payload.rangeproof.unwrap()
|
||||
);
|
||||
assert_eq!(payload.rangeproof.unwrap(), hops[4].rangeproof.unwrap());
|
||||
assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit);
|
||||
assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32));
|
||||
}
|
164
onion/src/util.rs
Normal file
164
onion/src/util.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
|
||||
/// Writes an optional value as '1' + value if Some, or '0' if None
|
||||
///
|
||||
/// This function is used to serialize an optional value into a Writer. If the option
|
||||
/// contains Some value, it writes '1' followed by the serialized value. If the option
|
||||
/// is None, it just writes '0'.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `writer` - A Writer instance where the data will be written.
|
||||
/// * `o` - The Optional value that will be written.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * If successful, returns Ok with nothing.
|
||||
/// * If an error occurs during writing, returns Err wrapping the error.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let mut writer = vec![];
|
||||
/// let optional_value: Option<u32> = Some(10);
|
||||
/// write_optional(&mut writer, &optional_value);
|
||||
/// assert_eq!(buf, &[1, 0, 0, 0, 10]);
|
||||
/// ```
|
||||
pub fn write_optional<O: Writeable, W: Writer>(
|
||||
writer: &mut W,
|
||||
o: &Option<O>,
|
||||
) -> Result<(), ser::Error> {
|
||||
match &o {
|
||||
Some(o) => {
|
||||
writer.write_u8(1)?;
|
||||
o.write(writer)?;
|
||||
}
|
||||
None => writer.write_u8(0)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads an optional value as '1' + value if Some, or '0' if None
|
||||
///
|
||||
/// This function is used to deserialize an optional value from a Reader. If the first byte
|
||||
/// read is '0', it returns None. If the first byte is '1', it reads the next value and
|
||||
/// returns Some(value).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reader` - A Reader instance from where the data will be read.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * If successful, returns Ok wrapping an optional value. If the first byte read was '0',
|
||||
/// returns None. If it was '1', returns Some(value).
|
||||
/// * If an error occurs during reading, returns Err wrapping the error.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let mut buf: &[u8] = &[1, 0, 0, 0, 10];
|
||||
/// let mut reader = BinReader::new(&mut buf, ProtocolVersion::local(), DeserializationMode::default());
|
||||
/// let optional_value: Option<u32> = read_optional(&mut reader).unwrap();
|
||||
/// assert_eq!(optional_value, Some(10));
|
||||
/// ```
|
||||
pub fn read_optional<O: Readable, R: Reader>(reader: &mut R) -> Result<Option<O>, ser::Error> {
|
||||
let o = if reader.read_u8()? == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(O::read(reader)?)
|
||||
};
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// Convert a vector to an array of size `S`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `vec` - The input vector.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * If successful, returns an `Ok` wrapping an array of size `S` containing
|
||||
/// the first `S` bytes of `vec`.
|
||||
/// * If `vec` is smaller than `S`, returns an `Err` indicating a count error.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let v = vec![0, 1, 2, 3, 4, 5];
|
||||
/// let a = vec_to_array::<4>(&v).unwrap();
|
||||
/// assert_eq!(a, [0, 1, 2, 3]);
|
||||
/// ```
|
||||
pub fn vec_to_array<const S: usize>(vec: &Vec<u8>) -> Result<[u8; S], ser::Error> {
|
||||
if vec.len() < S {
|
||||
return Err(ser::Error::CountError);
|
||||
}
|
||||
let arr: [u8; S] = vec[0..S].try_into().unwrap();
|
||||
Ok(arr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use grin_core::ser::{BinReader, BinWriter, DeserializationMode, ProtocolVersion};
|
||||
|
||||
#[test]
|
||||
fn test_write_optional() {
|
||||
// Test with Some value
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
let val: Option<u32> = Some(10);
|
||||
write_optional(&mut BinWriter::default(&mut buf), &val).unwrap();
|
||||
assert_eq!(buf, &[1, 0, 0, 0, 10]); // 1 for Some, then 10 as a little-endian u32
|
||||
|
||||
// Test with None value
|
||||
buf.clear();
|
||||
let val: Option<u32> = None;
|
||||
write_optional(&mut BinWriter::default(&mut buf), &val).unwrap();
|
||||
assert_eq!(buf, &[0]); // 0 for None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_optional() {
|
||||
// Test with Some value
|
||||
let mut buf: &[u8] = &[1, 0, 0, 0, 10]; // 1 for Some, then 10 as a little-endian u32
|
||||
let val: Option<u32> = read_optional(&mut BinReader::new(
|
||||
&mut buf,
|
||||
ProtocolVersion::local(),
|
||||
DeserializationMode::default(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(val, Some(10));
|
||||
|
||||
// Test with None value
|
||||
buf = &[0]; // 0 for None
|
||||
let val: Option<u32> = read_optional(&mut BinReader::new(
|
||||
&mut buf,
|
||||
ProtocolVersion::local(),
|
||||
DeserializationMode::default(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(val, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vec_to_array_success() {
|
||||
let v = vec![1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let a = vec_to_array::<4>(&v).unwrap();
|
||||
assert_eq!(a, [1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vec_to_array_too_small() {
|
||||
let v = vec![1, 2, 3];
|
||||
let res = vec_to_array::<4>(&v);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vec_to_array_empty() {
|
||||
let v = vec![];
|
||||
let res = vec_to_array::<4>(&v);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
171
src/client.rs
Normal file
171
src/client.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use crate::config::ServerConfig;
|
||||
use crate::crypto::dalek;
|
||||
use crate::servers::mix_rpc::MixReq;
|
||||
use crate::tx::TxComponents;
|
||||
use crate::{tor, DalekPublicKey};
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use grin_api::client;
|
||||
use grin_api::json_rpc::build_request;
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
||||
use serde_json;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error types for interacting with nodes
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("Tor Error: {0:?}")]
|
||||
Tor(tor::TorError),
|
||||
#[error("API Error: {0:?}")]
|
||||
API(grin_api::Error),
|
||||
#[error("Dalek Error: {0:?}")]
|
||||
Dalek(dalek::DalekError),
|
||||
#[error("Error parsing response: {0:?}")]
|
||||
ResponseParse(serde_json::Error),
|
||||
#[error("Custom client error: {0:?}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// A client for consuming a mix API
|
||||
pub trait MixClient: Send + Sync {
|
||||
/// Swaps the outputs provided and returns the final swapped outputs and kernels.
|
||||
fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<(Vec<usize>, TxComponents), ClientError>;
|
||||
}
|
||||
|
||||
pub struct MixClientImpl {
|
||||
config: ServerConfig,
|
||||
addr: OnionV3Address,
|
||||
}
|
||||
|
||||
impl MixClientImpl {
|
||||
pub fn new(config: ServerConfig, next_pubkey: DalekPublicKey) -> Self {
|
||||
let addr = OnionV3Address::from_bytes(next_pubkey.as_ref().to_bytes());
|
||||
MixClientImpl { config, addr }
|
||||
}
|
||||
|
||||
fn send_json_request<D: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
addr: &OnionV3Address,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<D, ClientError> {
|
||||
let _tor = tor::init_tor_sender(&self.config).map_err(ClientError::Tor)?;
|
||||
|
||||
let proxy = {
|
||||
let proxy_uri = format!("http://{:?}", self.config.socks_proxy_addr)
|
||||
.parse()
|
||||
.unwrap();
|
||||
let proxy = Proxy::new(Intercept::All, proxy_uri);
|
||||
//proxy.set_authorization(Authorization::basic("John Doe", "Agent1234"));
|
||||
let connector = HttpConnector::new();
|
||||
let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap();
|
||||
proxy_connector
|
||||
};
|
||||
|
||||
let url = format!("{}/v1", addr.to_http_str());
|
||||
let mut req = client::create_post_request(&url, None, &build_request(method, params))
|
||||
.map_err(ClientError::API)?;
|
||||
|
||||
let uri = url.parse().unwrap();
|
||||
if let Some(headers) = proxy.http_headers(&uri) {
|
||||
req.headers_mut().extend(headers.clone().into_iter());
|
||||
}
|
||||
|
||||
let res = client::send_request(req).map_err(ClientError::API)?;
|
||||
|
||||
serde_json::from_str(&res).map_err(ClientError::ResponseParse)
|
||||
}
|
||||
}
|
||||
|
||||
impl MixClient for MixClientImpl {
|
||||
fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<(Vec<usize>, TxComponents), ClientError> {
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
let sig =
|
||||
dalek::sign(&self.config.key, serialized.as_slice()).map_err(ClientError::Dalek)?;
|
||||
let mix = MixReq::new(onions.clone(), sig);
|
||||
|
||||
let params = serde_json::json!(mix);
|
||||
|
||||
self.send_json_request::<(Vec<usize>, TxComponents)>(&self.addr, "mix", ¶ms)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{ClientError, MixClient};
|
||||
use crate::tx::TxComponents;
|
||||
use grin_onion::onion::Onion;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct MockMixClient {
|
||||
results: HashMap<Vec<Onion>, (Vec<usize>, TxComponents)>,
|
||||
}
|
||||
|
||||
impl MockMixClient {
|
||||
pub fn new() -> MockMixClient {
|
||||
MockMixClient {
|
||||
results: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_response(&mut self, onions: &Vec<Onion>, r: (Vec<usize>, TxComponents)) {
|
||||
self.results.insert(onions.clone(), r);
|
||||
}
|
||||
}
|
||||
|
||||
impl MixClient for MockMixClient {
|
||||
fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
) -> Result<(Vec<usize>, TxComponents), ClientError> {
|
||||
self.results
|
||||
.get(onions)
|
||||
.map(|r| Ok(r.clone()))
|
||||
.unwrap_or(Err(ClientError::Custom("No response set for input".into())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use super::{ClientError, MixClient};
|
||||
use crate::crypto::dalek;
|
||||
use crate::crypto::secp::SecretKey;
|
||||
use crate::servers::mix::MixServer;
|
||||
use crate::tx::TxComponents;
|
||||
use crate::DalekPublicKey;
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
use grin_onion::onion::Onion;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Implementation of the 'MixClient' trait that calls a mix server implementation directly.
|
||||
/// No JSON-RPC serialization or socket communication occurs.
|
||||
#[derive(Clone)]
|
||||
pub struct DirectMixClient {
|
||||
pub key: SecretKey,
|
||||
pub mix_server: Arc<dyn MixServer>,
|
||||
}
|
||||
|
||||
impl MixClient for DirectMixClient {
|
||||
fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
) -> Result<(Vec<usize>, TxComponents), ClientError> {
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
let sig = dalek::sign(&self.key, serialized.as_slice()).map_err(ClientError::Dalek)?;
|
||||
|
||||
sig.verify(
|
||||
&DalekPublicKey::from_secret(&self.key),
|
||||
serialized.as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
Ok(self.mix_server.mix_outputs(&onions, &sig).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
use crate::secp::SecretKey;
|
||||
use crate::crypto::dalek::DalekPublicKey;
|
||||
use crate::crypto::secp::SecretKey;
|
||||
|
||||
use core::num::NonZeroU32;
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_util::{file, ToHex, ZeroingString};
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use rand::{thread_rng, Rng};
|
||||
use ring::{aead, pbkdf2};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
@ -26,6 +28,8 @@ pub struct ServerConfig {
|
|||
pub interval_s: u32,
|
||||
/// socket address the server listener should bind to
|
||||
pub addr: SocketAddr,
|
||||
/// socket address the tor sender should bind to
|
||||
pub socks_proxy_addr: SocketAddr,
|
||||
/// foreign api address of the grin node
|
||||
pub grin_node_url: SocketAddr,
|
||||
/// path to file containing api secret for the grin node
|
||||
|
@ -34,9 +38,23 @@ pub struct ServerConfig {
|
|||
pub wallet_owner_url: SocketAddr,
|
||||
/// path to file containing secret for the grin wallet's owner api
|
||||
pub wallet_owner_secret_path: Option<String>,
|
||||
/// public key of the previous mix/swap server (e.g. N_1 if this is N_2)
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
pub prev_server: Option<DalekPublicKey>,
|
||||
/// public key of the next mix server
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
pub next_server: Option<DalekPublicKey>,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn onion_address(&self) -> OnionV3Address {
|
||||
OnionV3Address::from_private(&self.key.0).unwrap()
|
||||
}
|
||||
|
||||
pub fn server_pubkey(&self) -> DalekPublicKey {
|
||||
DalekPublicKey::from_secret(&self.key)
|
||||
}
|
||||
|
||||
pub fn node_api_secret(&self) -> Option<String> {
|
||||
file::get_first_line(self.grin_node_secret_path.clone())
|
||||
}
|
||||
|
@ -163,10 +181,15 @@ struct RawConfig {
|
|||
nonce: String,
|
||||
interval_s: u32,
|
||||
addr: SocketAddr,
|
||||
socks_proxy_addr: SocketAddr,
|
||||
grin_node_url: SocketAddr,
|
||||
grin_node_secret_path: Option<String>,
|
||||
wallet_owner_url: SocketAddr,
|
||||
wallet_owner_secret_path: Option<String>,
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
prev_server: Option<DalekPublicKey>,
|
||||
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
|
||||
next_server: Option<DalekPublicKey>,
|
||||
}
|
||||
|
||||
/// Writes the server config to the config_path given, encrypting the server_key first.
|
||||
|
@ -183,10 +206,13 @@ pub fn write_config(
|
|||
nonce: encrypted.nonce,
|
||||
interval_s: server_config.interval_s,
|
||||
addr: server_config.addr,
|
||||
socks_proxy_addr: server_config.socks_proxy_addr,
|
||||
grin_node_url: server_config.grin_node_url,
|
||||
grin_node_secret_path: server_config.grin_node_secret_path.clone(),
|
||||
wallet_owner_url: server_config.wallet_owner_url,
|
||||
wallet_owner_secret_path: server_config.wallet_owner_secret_path.clone(),
|
||||
prev_server: server_config.prev_server.clone(),
|
||||
next_server: server_config.next_server.clone(),
|
||||
};
|
||||
let encoded: String =
|
||||
toml::to_string(&raw_config).map_err(|e| ConfigError::EncodingError(e))?;
|
||||
|
@ -203,10 +229,8 @@ pub fn load_config(
|
|||
config_path: &PathBuf,
|
||||
password: &ZeroingString,
|
||||
) -> Result<ServerConfig, ConfigError> {
|
||||
let contents =
|
||||
std::fs::read_to_string(config_path).map_err(|e| ConfigError::ReadConfigError(e))?;
|
||||
let raw_config: RawConfig =
|
||||
toml::from_str(&contents).map_err(|e| ConfigError::DecodingError(e))?;
|
||||
let contents = std::fs::read_to_string(config_path).map_err(ConfigError::ReadConfigError)?;
|
||||
let raw_config: RawConfig = toml::from_str(&contents).map_err(ConfigError::DecodingError)?;
|
||||
|
||||
let encrypted_key = EncryptedServerKey {
|
||||
encrypted_key: raw_config.encrypted_key,
|
||||
|
@ -219,10 +243,13 @@ pub fn load_config(
|
|||
key: secret_key,
|
||||
interval_s: raw_config.interval_s,
|
||||
addr: raw_config.addr,
|
||||
socks_proxy_addr: raw_config.socks_proxy_addr,
|
||||
grin_node_url: raw_config.grin_node_url,
|
||||
grin_node_secret_path: raw_config.grin_node_secret_path,
|
||||
wallet_owner_url: raw_config.wallet_owner_url,
|
||||
wallet_owner_secret_path: raw_config.wallet_owner_secret_path,
|
||||
prev_server: raw_config.prev_server,
|
||||
next_server: raw_config.next_server,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -260,10 +287,37 @@ pub fn wallet_owner_url(_chain_type: &ChainTypes) -> SocketAddr {
|
|||
"127.0.0.1:3420".parse().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use crate::{DalekPublicKey, ServerConfig};
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::net::TcpListener;
|
||||
|
||||
pub fn local_config(
|
||||
server_key: &SecretKey,
|
||||
prev_server: &Option<DalekPublicKey>,
|
||||
next_server: &Option<DalekPublicKey>,
|
||||
) -> Result<ServerConfig, Box<dyn std::error::Error>> {
|
||||
let config = ServerConfig {
|
||||
key: server_key.clone(),
|
||||
interval_s: 1,
|
||||
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
socks_proxy_addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
grin_node_url: "127.0.0.1:3413".parse()?,
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: "127.0.0.1:3420".parse()?,
|
||||
wallet_owner_secret_path: None,
|
||||
prev_server: prev_server.clone(),
|
||||
next_server: next_server.clone(),
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::secp;
|
||||
use crate::crypto::secp;
|
||||
|
||||
#[test]
|
||||
fn server_key_encrypt() {
|
||||
|
|
147
src/main.rs
147
src/main.rs
|
@ -1,18 +1,17 @@
|
|||
use config::ServerConfig;
|
||||
use node::HttpGrinNode;
|
||||
use std::collections::HashMap;
|
||||
use store::SwapStore;
|
||||
use wallet::HttpWallet;
|
||||
|
||||
use crate::client::{MixClient, MixClientImpl};
|
||||
use crate::node::GrinNode;
|
||||
use crate::store::StoreError;
|
||||
use clap::App;
|
||||
use grin_core::global;
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_onion::crypto;
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use grin_util::{StopState, ZeroingString};
|
||||
use grin_wallet_impls::tor::config as tor_config;
|
||||
use grin_wallet_impls::tor::process as tor_process;
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use rpassword;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
@ -21,20 +20,19 @@ use tokio::runtime::Runtime;
|
|||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
mod client;
|
||||
mod config;
|
||||
mod node;
|
||||
mod onion;
|
||||
mod rpc;
|
||||
mod secp;
|
||||
mod server;
|
||||
mod servers;
|
||||
mod store;
|
||||
mod types;
|
||||
mod tor;
|
||||
mod tx;
|
||||
mod wallet;
|
||||
|
||||
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
|
||||
|
||||
fn main() {
|
||||
real_main().unwrap();
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
real_main()?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
|
@ -61,10 +59,17 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.value_of("round_time")
|
||||
.map(|t| t.parse::<u32>().unwrap());
|
||||
let bind_addr = args.value_of("bind_addr");
|
||||
let socks_addr = args.value_of("socks_addr");
|
||||
let grin_node_url = args.value_of("grin_node_url");
|
||||
let grin_node_secret_path = args.value_of("grin_node_secret_path");
|
||||
let wallet_owner_url = args.value_of("wallet_owner_url");
|
||||
let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path");
|
||||
let prev_server = args
|
||||
.value_of("prev_server")
|
||||
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
|
||||
let next_server = args
|
||||
.value_of("next_server")
|
||||
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
|
||||
|
||||
// Write a new config file if init-config command is supplied
|
||||
if let ("init-config", Some(_)) = args.subcommand() {
|
||||
|
@ -76,9 +81,10 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
let server_config = ServerConfig {
|
||||
key: secp::random_secret(),
|
||||
key: crypto::secp::random_secret(),
|
||||
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
|
||||
addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?,
|
||||
socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?,
|
||||
grin_node_url: match grin_node_url {
|
||||
Some(u) => u.parse()?,
|
||||
None => config::grin_node_url(&chain_type),
|
||||
|
@ -99,6 +105,8 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.to_str()
|
||||
.map(|p| p.to_owned()),
|
||||
},
|
||||
prev_server,
|
||||
next_server,
|
||||
};
|
||||
|
||||
let password = prompt_password_confirm();
|
||||
|
@ -113,11 +121,6 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let password = prompt_password();
|
||||
let mut server_config = config::load_config(&config_path, &password)?;
|
||||
|
||||
// Override bind_addr, if supplied
|
||||
if let Some(bind_addr) = bind_addr {
|
||||
server_config.addr = bind_addr.parse()?;
|
||||
}
|
||||
|
||||
// Override grin_node_url, if supplied
|
||||
if let Some(grin_node_url) = grin_node_url {
|
||||
server_config.grin_node_url = grin_node_url.parse()?;
|
||||
|
@ -138,6 +141,26 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned());
|
||||
}
|
||||
|
||||
// Override bind_addr, if supplied
|
||||
if let Some(bind_addr) = bind_addr {
|
||||
server_config.addr = bind_addr.parse()?;
|
||||
}
|
||||
|
||||
// Override socks_addr, if supplied
|
||||
if let Some(socks_addr) = socks_addr {
|
||||
server_config.socks_proxy_addr = socks_addr.parse()?;
|
||||
}
|
||||
|
||||
// Override prev_server, if supplied
|
||||
if let Some(prev_server) = prev_server {
|
||||
server_config.prev_server = Some(prev_server);
|
||||
}
|
||||
|
||||
// Override next_server, if supplied
|
||||
if let Some(next_server) = next_server {
|
||||
server_config.next_server = Some(next_server);
|
||||
}
|
||||
|
||||
// Create GrinNode
|
||||
let node = HttpGrinNode::new(
|
||||
&server_config.grin_node_url,
|
||||
|
@ -165,17 +188,7 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
};
|
||||
|
||||
// Open SwapStore
|
||||
let store = SwapStore::new(
|
||||
config::get_grin_path(&chain_type) // todo: load from config
|
||||
.join("db")
|
||||
.to_str()
|
||||
.ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr(
|
||||
"db_root path error".to_string(),
|
||||
)))?,
|
||||
)?;
|
||||
|
||||
let mut tor_process = init_tor_listener(&server_config)?;
|
||||
let mut tor_process = tor::init_tor_listener(&server_config)?;
|
||||
|
||||
let stop_state = Arc::new(StopState::new());
|
||||
let stop_state_clone = stop_state.clone();
|
||||
|
@ -187,15 +200,53 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
stop_state_clone.stop();
|
||||
});
|
||||
|
||||
// Start the mwixnet JSON-RPC HTTP server
|
||||
rpc::listen(
|
||||
let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| {
|
||||
let client: Arc<dyn MixClient> =
|
||||
Arc::new(MixClientImpl::new(server_config.clone(), pk.clone()));
|
||||
client
|
||||
});
|
||||
|
||||
if server_config.prev_server.is_some() {
|
||||
// Start the JSON-RPC HTTP 'mix' server
|
||||
println!(
|
||||
"Starting MIX server with public key {:?}",
|
||||
server_config.server_pubkey().to_hex()
|
||||
);
|
||||
|
||||
servers::mix_rpc::listen(
|
||||
server_config,
|
||||
next_mixer,
|
||||
Arc::new(wallet),
|
||||
Arc::new(node),
|
||||
stop_state,
|
||||
)
|
||||
} else {
|
||||
println!(
|
||||
"Starting SWAP server with public key {:?}",
|
||||
server_config.server_pubkey().to_hex()
|
||||
);
|
||||
|
||||
// Open SwapStore
|
||||
let store = SwapStore::new(
|
||||
config::get_grin_path(&chain_type)
|
||||
.join("db")
|
||||
.to_str()
|
||||
.ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr(
|
||||
"db_root path error".to_string(),
|
||||
)))?,
|
||||
)?;
|
||||
|
||||
// Start the mwixnet JSON-RPC HTTP 'swap' server
|
||||
servers::swap_rpc::listen(
|
||||
server_config,
|
||||
next_mixer,
|
||||
Arc::new(wallet),
|
||||
Arc::new(node),
|
||||
store,
|
||||
stop_state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn build_signals_fut() {
|
||||
|
@ -245,39 +296,3 @@ fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tor_listener(
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<tor_process::TorProcess, Box<dyn std::error::Error>> {
|
||||
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
|
||||
tor_dir.push("tor/listener");
|
||||
|
||||
let mut torrc_dir = tor_dir.clone();
|
||||
torrc_dir.push("torrc");
|
||||
|
||||
tor_config::output_tor_listener_config(
|
||||
tor_dir.to_str().unwrap(),
|
||||
server_config.addr.to_string().as_str(),
|
||||
&vec![server_config.key.clone()],
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Start TOR process
|
||||
let mut process = tor_process::TorProcess::new();
|
||||
process
|
||||
.torrc_path(torrc_dir.to_str().unwrap())
|
||||
.working_dir(tor_dir.to_str().unwrap())
|
||||
.timeout(20)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.unwrap();
|
||||
|
||||
let onion_address = OnionV3Address::from_private(&server_config.key.0).unwrap();
|
||||
println!(
|
||||
"Server listening at http://{}.onion",
|
||||
onion_address.to_ov3_str()
|
||||
);
|
||||
Ok(process)
|
||||
}
|
||||
|
|
21
src/node.rs
21
src/node.rs
|
@ -1,4 +1,4 @@
|
|||
use crate::secp::Commitment;
|
||||
use crate::crypto::secp::Commitment;
|
||||
|
||||
use grin_api::client;
|
||||
use grin_api::json_rpc::{build_request, Request, Response};
|
||||
|
@ -43,10 +43,10 @@ pub fn is_unspent(node: &Arc<dyn GrinNode>, commit: &Commitment) -> Result<bool,
|
|||
/// Checks whether a commitment is spendable at the block height provided
|
||||
pub fn is_spendable(
|
||||
node: &Arc<dyn GrinNode>,
|
||||
output_commit: &Commitment,
|
||||
commit: &Commitment,
|
||||
next_block_height: u64,
|
||||
) -> Result<bool, NodeError> {
|
||||
let output = node.get_utxo(&output_commit)?;
|
||||
let output = node.get_utxo(&commit)?;
|
||||
if let Some(out) = output {
|
||||
let is_coinbase = match out.output_type {
|
||||
OutputType::Coinbase => true,
|
||||
|
@ -166,7 +166,7 @@ impl GrinNode for HttpGrinNode {
|
|||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{GrinNode, NodeError};
|
||||
use crate::secp::Commitment;
|
||||
use crate::crypto::secp::Commitment;
|
||||
|
||||
use grin_api::{OutputPrintable, OutputType};
|
||||
use grin_core::core::Transaction;
|
||||
|
@ -181,13 +181,24 @@ pub mod mock {
|
|||
}
|
||||
|
||||
impl MockGrinNode {
|
||||
pub fn new() -> MockGrinNode {
|
||||
pub fn new() -> Self {
|
||||
MockGrinNode {
|
||||
utxos: HashMap::new(),
|
||||
txns_posted: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_utxos(utxos: &Vec<&Commitment>) -> Self {
|
||||
let mut node = MockGrinNode {
|
||||
utxos: HashMap::new(),
|
||||
txns_posted: RwLock::new(Vec::new()),
|
||||
};
|
||||
for utxo in utxos {
|
||||
node.add_default_utxo(utxo);
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) {
|
||||
self.utxos.insert(output_commit.clone(), utxo.clone());
|
||||
}
|
||||
|
|
450
src/servers/mix.rs
Normal file
450
src/servers/mix.rs
Normal file
|
@ -0,0 +1,450 @@
|
|||
use crate::client::MixClient;
|
||||
use crate::config::ServerConfig;
|
||||
use crate::tx::TxComponents;
|
||||
use crate::wallet::Wallet;
|
||||
use crate::{node, tx, GrinNode};
|
||||
|
||||
use grin_core::core::{Output, OutputFeatures, TransactionBody};
|
||||
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
||||
use grin_core::ser;
|
||||
use grin_core::ser::ProtocolVersion;
|
||||
use grin_onion::crypto::dalek::{self, DalekSignature};
|
||||
use grin_onion::onion::{Onion, OnionError, PeeledOnion};
|
||||
use itertools::Itertools;
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use secp256k1zkp::Secp256k1;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Mixer error types
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MixError {
|
||||
#[error("Invalid number of payloads provided")]
|
||||
InvalidPayloadLength,
|
||||
#[error("Signature is invalid")]
|
||||
InvalidSignature,
|
||||
#[error("Rangeproof is invalid")]
|
||||
InvalidRangeproof,
|
||||
#[error("Rangeproof is required but was not supplied")]
|
||||
MissingRangeproof,
|
||||
#[error("Failed to peel onion layer: {0:?}")]
|
||||
PeelOnionFailure(OnionError),
|
||||
#[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")]
|
||||
FeeTooLow { minimum_fee: u64, actual_fee: u64 },
|
||||
#[error("None of the outputs could be mixed")]
|
||||
NoValidOutputs,
|
||||
#[error("Dalek error: {0:?}")]
|
||||
Dalek(dalek::DalekError),
|
||||
#[error("Secp error: {0:?}")]
|
||||
Secp(grin_util::secp::Error),
|
||||
#[error("Error building transaction: {0:?}")]
|
||||
TxError(tx::TxError),
|
||||
#[error("Wallet error: {0:?}")]
|
||||
WalletError(crate::wallet::WalletError),
|
||||
#[error("Client comm error: {0:?}")]
|
||||
Client(crate::client::ClientError),
|
||||
}
|
||||
|
||||
/// An internal MWixnet server - a "Mixer"
|
||||
pub trait MixServer: Send + Sync {
|
||||
/// Swaps the outputs provided and returns the final swapped outputs and kernels.
|
||||
fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
sig: &DalekSignature,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError>;
|
||||
}
|
||||
|
||||
/// The standard MWixnet "Mixer" implementation
|
||||
#[derive(Clone)]
|
||||
pub struct MixServerImpl {
|
||||
secp: Secp256k1,
|
||||
server_config: ServerConfig,
|
||||
mix_client: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
}
|
||||
|
||||
impl MixServerImpl {
|
||||
/// Create a new 'Mix' server
|
||||
pub fn new(
|
||||
server_config: ServerConfig,
|
||||
mix_client: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
) -> Self {
|
||||
MixServerImpl {
|
||||
secp: Secp256k1::new(),
|
||||
server_config,
|
||||
mix_client,
|
||||
wallet,
|
||||
node,
|
||||
}
|
||||
}
|
||||
|
||||
/// The fee base to use. For now, just using the default.
|
||||
fn get_fee_base(&self) -> u64 {
|
||||
DEFAULT_ACCEPT_FEE_BASE
|
||||
}
|
||||
|
||||
/// Minimum fee to perform a mix.
|
||||
/// Requires enough fee for the mixer's kernel.
|
||||
fn get_minimum_mix_fee(&self) -> u64 {
|
||||
TransactionBody::weight_by_iok(0, 0, 1) * self.get_fee_base()
|
||||
}
|
||||
|
||||
fn peel_onion(&self, onion: &Onion) -> Result<PeeledOnion, MixError> {
|
||||
// Verify that more than 1 payload exists when there's a next server,
|
||||
// or that exactly 1 payload exists when this is the final server
|
||||
if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1
|
||||
|| self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1
|
||||
{
|
||||
return Err(MixError::InvalidPayloadLength);
|
||||
}
|
||||
|
||||
// Peel the top layer
|
||||
let peeled = onion
|
||||
.peel_layer(&self.server_config.key)
|
||||
.map_err(|e| MixError::PeelOnionFailure(e))?;
|
||||
|
||||
// Verify the fee meets the minimum
|
||||
let fee: u64 = peeled.payload.fee.into();
|
||||
if fee < self.get_minimum_mix_fee() {
|
||||
return Err(MixError::FeeTooLow {
|
||||
minimum_fee: self.get_minimum_mix_fee(),
|
||||
actual_fee: fee,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(r) = peeled.payload.rangeproof {
|
||||
// Verify the bullet proof
|
||||
self.secp
|
||||
.verify_bullet_proof(peeled.onion.commit, r, None)
|
||||
.map_err(|_| MixError::InvalidRangeproof)?;
|
||||
} else if peeled.onion.enc_payloads.is_empty() {
|
||||
// A rangeproof is required in the last payload
|
||||
return Err(MixError::MissingRangeproof);
|
||||
}
|
||||
|
||||
Ok(peeled)
|
||||
}
|
||||
|
||||
fn build_final_outputs(
|
||||
&self,
|
||||
peeled: &Vec<(usize, PeeledOnion)>,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError> {
|
||||
// Filter out commitments that already exist in the UTXO set
|
||||
let filtered: Vec<&(usize, PeeledOnion)> = peeled
|
||||
.iter()
|
||||
.filter(|(_, p)| !node::is_unspent(&self.node, &p.onion.commit).unwrap_or(true))
|
||||
.collect();
|
||||
|
||||
// Build plain outputs for each mix entry
|
||||
let outputs: Vec<Output> = filtered
|
||||
.iter()
|
||||
.map(|(_, p)| {
|
||||
Output::new(
|
||||
OutputFeatures::Plain,
|
||||
p.onion.commit,
|
||||
p.payload.rangeproof.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let fees_paid = filtered.iter().map(|(_, p)| p.payload.fee.fee()).sum();
|
||||
let output_excesses = filtered
|
||||
.iter()
|
||||
.map(|(_, p)| p.payload.excess.clone())
|
||||
.collect();
|
||||
|
||||
let components = tx::assemble_components(
|
||||
&self.wallet,
|
||||
&TxComponents {
|
||||
offset: ZERO_KEY,
|
||||
kernels: Vec::new(),
|
||||
outputs,
|
||||
},
|
||||
&output_excesses,
|
||||
self.get_fee_base(),
|
||||
fees_paid,
|
||||
)
|
||||
.map_err(MixError::TxError)?;
|
||||
|
||||
let indices = filtered.iter().map(|(i, _)| *i).collect();
|
||||
|
||||
Ok((indices, components))
|
||||
}
|
||||
|
||||
fn call_next_mixer(
|
||||
&self,
|
||||
peeled: &Vec<(usize, PeeledOnion)>,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError> {
|
||||
// Sort by commitment
|
||||
let mut onions_with_index = peeled.clone();
|
||||
onions_with_index
|
||||
.sort_by(|(_, a), (_, b)| a.onion.commit.partial_cmp(&b.onion.commit).unwrap());
|
||||
|
||||
// Create map of prev indices to next indices
|
||||
let map_indices: HashMap<usize, usize> =
|
||||
HashMap::from_iter(onions_with_index.iter().enumerate().map(|(i, j)| (j.0, i)));
|
||||
|
||||
// Call next server
|
||||
let onions = peeled.iter().map(|(_, p)| p.onion.clone()).collect();
|
||||
let (mixed_indices, mixed_components) = self
|
||||
.mix_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mix_outputs(&onions)
|
||||
.map_err(MixError::Client)?;
|
||||
|
||||
// Remove filtered entries
|
||||
let kept_next_indices = HashSet::<_>::from_iter(mixed_indices.clone());
|
||||
let filtered_onions: Vec<&(usize, PeeledOnion)> = onions_with_index
|
||||
.iter()
|
||||
.filter(|(i, _)| {
|
||||
map_indices.contains_key(i)
|
||||
&& kept_next_indices.contains(map_indices.get(i).unwrap())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Calculate excess of entries kept
|
||||
let excesses = filtered_onions
|
||||
.iter()
|
||||
.map(|(_, p)| p.payload.excess.clone())
|
||||
.collect();
|
||||
|
||||
// Calculate total fee of entries kept
|
||||
let fees_paid = filtered_onions
|
||||
.iter()
|
||||
.fold(0, |f, (_, p)| f + p.payload.fee.fee());
|
||||
|
||||
let indices = kept_next_indices.into_iter().sorted().collect();
|
||||
|
||||
let components = tx::assemble_components(
|
||||
&self.wallet,
|
||||
&mixed_components,
|
||||
&excesses,
|
||||
self.get_fee_base(),
|
||||
fees_paid,
|
||||
)
|
||||
.map_err(MixError::TxError)?;
|
||||
|
||||
Ok((indices, components))
|
||||
}
|
||||
}
|
||||
|
||||
impl MixServer for MixServerImpl {
|
||||
fn mix_outputs(
|
||||
&self,
|
||||
onions: &Vec<Onion>,
|
||||
sig: &DalekSignature,
|
||||
) -> Result<(Vec<usize>, TxComponents), MixError> {
|
||||
// Verify Signature
|
||||
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
|
||||
sig.verify(
|
||||
self.server_config.prev_server.as_ref().unwrap(),
|
||||
serialized.as_slice(),
|
||||
)
|
||||
.map_err(|_| MixError::InvalidSignature)?;
|
||||
|
||||
// Peel onions and filter
|
||||
let mut peeled: Vec<(usize, PeeledOnion)> = onions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, o)| {
|
||||
if let Some(p) = self.peel_onion(&o).ok() {
|
||||
Some((i, p))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Remove duplicate commitments
|
||||
peeled.sort_by_key(|(_, o)| o.onion.commit);
|
||||
peeled.dedup_by_key(|(_, o)| o.onion.commit);
|
||||
peeled.sort_by_key(|(i, _)| *i);
|
||||
|
||||
if peeled.is_empty() {
|
||||
return Err(MixError::NoValidOutputs);
|
||||
}
|
||||
|
||||
if self.server_config.next_server.is_some() {
|
||||
self.call_next_mixer(&peeled)
|
||||
} else {
|
||||
self.build_final_outputs(&peeled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_util {
|
||||
use crate::client::test_util::DirectMixClient;
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::wallet::mock::MockWallet;
|
||||
use crate::{config, DalekPublicKey, MixClient};
|
||||
|
||||
use crate::servers::mix::MixServerImpl;
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn new_mixer(
|
||||
server_key: &SecretKey,
|
||||
prev_server: (&SecretKey, &DalekPublicKey),
|
||||
next_server: &Option<(DalekPublicKey, Arc<dyn MixClient>)>,
|
||||
node: &Arc<MockGrinNode>,
|
||||
) -> (Arc<DirectMixClient>, Arc<MockWallet>) {
|
||||
let config = config::test_util::local_config(
|
||||
&server_key,
|
||||
&Some(prev_server.1.clone()),
|
||||
&next_server.as_ref().map(|(k, _)| k.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let wallet = Arc::new(MockWallet::new());
|
||||
let mix_server = Arc::new(MixServerImpl::new(
|
||||
config,
|
||||
next_server.as_ref().map(|(_, c)| c.clone()),
|
||||
wallet.clone(),
|
||||
node.clone(),
|
||||
));
|
||||
let client = Arc::new(DirectMixClient {
|
||||
key: prev_server.0.clone(),
|
||||
mix_server: mix_server.clone(),
|
||||
});
|
||||
|
||||
(client, wallet)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::{DalekPublicKey, MixClient};
|
||||
|
||||
use ::function_name::named;
|
||||
use grin_onion::crypto::secp::{self, Commitment};
|
||||
use grin_onion::test_util as onion_test_util;
|
||||
use grin_onion::{create_onion, new_hop, Hop};
|
||||
use secp256k1zkp::pedersen::RangeProof;
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
macro_rules! init_test {
|
||||
() => {{
|
||||
grin_core::global::set_local_chain_type(
|
||||
grin_core::global::ChainTypes::AutomatedTesting,
|
||||
);
|
||||
let db_root = concat!("./target/tmp/.", function_name!());
|
||||
let _ = std::fs::remove_dir_all(db_root);
|
||||
()
|
||||
}};
|
||||
}
|
||||
|
||||
struct ServerVars {
|
||||
fee: u32,
|
||||
sk: SecretKey,
|
||||
pk: DalekPublicKey,
|
||||
excess: SecretKey,
|
||||
}
|
||||
|
||||
impl ServerVars {
|
||||
fn new(fee: u32) -> Self {
|
||||
let (sk, pk) = onion_test_util::rand_keypair();
|
||||
let excess = secp::random_secret();
|
||||
ServerVars {
|
||||
fee,
|
||||
sk,
|
||||
pk,
|
||||
excess,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hop(&self, proof: Option<RangeProof>) -> Hop {
|
||||
new_hop(&self.sk, &self.excess, self.fee, proof)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests the happy path for a 3 server setup.
|
||||
///
|
||||
/// Servers:
|
||||
/// * Swap Server - Simulated by test
|
||||
/// * Mixer 1 - Internal MixServerImpl directly called by test
|
||||
/// * Mixer 2 - Final MixServerImpl called by Mixer 1
|
||||
#[test]
|
||||
#[named]
|
||||
fn mix_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
||||
init_test!();
|
||||
|
||||
// Setup Input(s)
|
||||
let input1_value: u64 = 200_000_000;
|
||||
let input1_blind = secp::random_secret();
|
||||
let input1_commit = secp::commit(input1_value, &input1_blind)?;
|
||||
let input_commits = vec![&input1_commit];
|
||||
|
||||
// Setup Servers
|
||||
let (swap_vars, mix1_vars, mix2_vars) = (
|
||||
ServerVars::new(50_000_000),
|
||||
ServerVars::new(50_000_000),
|
||||
ServerVars::new(50_000_000),
|
||||
);
|
||||
|
||||
let node = Arc::new(MockGrinNode::new_with_utxos(&input_commits));
|
||||
let (mixer2_client, mixer2_wallet) = super::test_util::new_mixer(
|
||||
&mix2_vars.sk,
|
||||
(&mix1_vars.sk, &mix1_vars.pk),
|
||||
&None,
|
||||
&node,
|
||||
);
|
||||
|
||||
let (mixer1_client, mixer1_wallet) = super::test_util::new_mixer(
|
||||
&mix1_vars.sk,
|
||||
(&swap_vars.sk, &swap_vars.pk),
|
||||
&Some((mix2_vars.pk.clone(), mixer2_client.clone())),
|
||||
&node,
|
||||
);
|
||||
|
||||
// Build rangeproof
|
||||
let (output_commit, proof) = onion_test_util::proof(
|
||||
input1_value,
|
||||
swap_vars.fee + mix1_vars.fee + mix2_vars.fee,
|
||||
&input1_blind,
|
||||
&vec![&swap_vars.excess, &mix1_vars.excess, &mix2_vars.excess],
|
||||
);
|
||||
|
||||
// Create Onion
|
||||
let onion = create_onion(
|
||||
&input1_commit,
|
||||
&vec![
|
||||
swap_vars.build_hop(None),
|
||||
mix1_vars.build_hop(None),
|
||||
mix2_vars.build_hop(Some(proof)),
|
||||
],
|
||||
)?;
|
||||
|
||||
// Simulate the swap server peeling the onion and then calling mix1
|
||||
let mix1_onion = onion.peel_layer(&swap_vars.sk)?;
|
||||
let (mixed_indices, mixed_components) =
|
||||
mixer1_client.mix_outputs(&vec![mix1_onion.onion.clone()])?;
|
||||
|
||||
// Verify 3 outputs are returned: mixed output, mixer1's output, and mixer2's output
|
||||
assert_eq!(mixed_indices, vec![0 as usize]);
|
||||
assert_eq!(mixed_components.outputs.len(), 3);
|
||||
let output_commits: HashSet<Commitment> = mixed_components
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|o| o.identifier.commit.clone())
|
||||
.collect();
|
||||
assert!(output_commits.contains(&output_commit));
|
||||
|
||||
assert_eq!(mixer1_wallet.built_outputs().len(), 1);
|
||||
assert!(output_commits.contains(mixer1_wallet.built_outputs().get(0).unwrap()));
|
||||
|
||||
assert_eq!(mixer2_wallet.built_outputs().len(), 1);
|
||||
assert!(output_commits.contains(mixer2_wallet.built_outputs().get(0).unwrap()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
116
src/servers/mix_rpc.rs
Normal file
116
src/servers/mix_rpc.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use crate::client::MixClient;
|
||||
use crate::config::ServerConfig;
|
||||
use crate::crypto::dalek::{self, DalekSignature};
|
||||
use crate::node::GrinNode;
|
||||
use crate::servers::mix::{MixError, MixServer, MixServerImpl};
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_util::StopState;
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpc_http_server::jsonrpc_core::{self as jsonrpc, IoHandler};
|
||||
use jsonrpc_http_server::{DomainsValidation, ServerBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{sleep, spawn};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MixReq {
|
||||
onions: Vec<Onion>,
|
||||
#[serde(with = "dalek::dalek_sig_serde")]
|
||||
sig: DalekSignature,
|
||||
}
|
||||
|
||||
impl MixReq {
|
||||
pub fn new(onions: Vec<Onion>, sig: DalekSignature) -> Self {
|
||||
MixReq { onions, sig }
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(server)]
|
||||
pub trait MixAPI {
|
||||
#[rpc(name = "mix")]
|
||||
fn mix(&self, mix: MixReq) -> jsonrpc::Result<jsonrpc::Value>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RPCMixServer {
|
||||
server_config: ServerConfig,
|
||||
server: Arc<Mutex<dyn MixServer>>,
|
||||
}
|
||||
|
||||
impl RPCMixServer {
|
||||
/// Spin up an instance of the JSON-RPC HTTP server.
|
||||
fn start_http(&self) -> jsonrpc_http_server::Server {
|
||||
let mut io = IoHandler::new();
|
||||
io.extend_with(RPCMixServer::to_delegate(self.clone()));
|
||||
|
||||
ServerBuilder::new(io)
|
||||
.cors(DomainsValidation::Disabled)
|
||||
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
||||
if request.uri() == "/v1" {
|
||||
request.into()
|
||||
} else {
|
||||
jsonrpc_http_server::Response::bad_request("Only v1 supported").into()
|
||||
}
|
||||
})
|
||||
.start_http(&self.server_config.addr)
|
||||
.expect("Unable to start RPC server")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MixError> for jsonrpc::Error {
|
||||
fn from(e: MixError) -> Self {
|
||||
jsonrpc::Error::invalid_params(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl MixAPI for RPCMixServer {
|
||||
fn mix(&self, mix: MixReq) -> jsonrpc::Result<jsonrpc::Value> {
|
||||
self.server
|
||||
.lock()
|
||||
.unwrap()
|
||||
.mix_outputs(&mix.onions, &mix.sig)?;
|
||||
Ok(jsonrpc::Value::String("success".into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Spin up the JSON-RPC web server
|
||||
pub fn listen(
|
||||
server_config: ServerConfig,
|
||||
next_server: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
stop_state: Arc<StopState>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let server = MixServerImpl::new(
|
||||
server_config.clone(),
|
||||
next_server,
|
||||
wallet.clone(),
|
||||
node.clone(),
|
||||
);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
let rpc_server = RPCMixServer {
|
||||
server_config: server_config.clone(),
|
||||
server: server.clone(),
|
||||
};
|
||||
|
||||
let http_server = rpc_server.start_http();
|
||||
|
||||
let close_handle = http_server.close_handle();
|
||||
let round_handle = spawn(move || loop {
|
||||
if stop_state.is_stopped() {
|
||||
close_handle.close();
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100));
|
||||
});
|
||||
|
||||
http_server.wait();
|
||||
round_handle.join().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
4
src/servers/mod.rs
Normal file
4
src/servers/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod mix;
|
||||
pub mod mix_rpc;
|
||||
pub mod swap;
|
||||
pub mod swap_rpc;
|
|
@ -1,14 +1,19 @@
|
|||
use crate::client::MixClient;
|
||||
use crate::config::ServerConfig;
|
||||
use crate::crypto::comsig::ComSignature;
|
||||
use crate::crypto::secp::{Commitment, Secp256k1, SecretKey};
|
||||
use crate::node::{self, GrinNode};
|
||||
use crate::onion::{Onion, OnionError};
|
||||
use crate::secp::{ComSignature, Commitment, Secp256k1, SecretKey};
|
||||
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
|
||||
use crate::wallet::{self, Wallet};
|
||||
use crate::tx;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_core::core::hash::Hashed;
|
||||
use grin_core::core::{Input, Output, OutputFeatures, Transaction, TransactionBody};
|
||||
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
||||
use grin_onion::onion::{Onion, OnionError};
|
||||
use itertools::Itertools;
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use std::collections::HashSet;
|
||||
use std::result::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use thiserror::Error;
|
||||
|
@ -16,8 +21,8 @@ use thiserror::Error;
|
|||
/// Swap error types
|
||||
#[derive(Clone, Error, Debug, PartialEq)]
|
||||
pub enum SwapError {
|
||||
#[error("Invalid number of payloads provided (expected {expected:?}, found {found:?})")]
|
||||
InvalidPayloadLength { expected: usize, found: usize },
|
||||
#[error("Invalid number of payloads provided")]
|
||||
InvalidPayloadLength,
|
||||
#[error("Commitment Signature is invalid")]
|
||||
InvalidComSignature,
|
||||
#[error("Rangeproof is invalid")]
|
||||
|
@ -34,41 +39,44 @@ pub enum SwapError {
|
|||
FeeTooLow { minimum_fee: u64, actual_fee: u64 },
|
||||
#[error("Error saving swap to data store: {0}")]
|
||||
StoreError(StoreError),
|
||||
#[error("Client communication error: {0:?}")]
|
||||
ClientError(String),
|
||||
#[error("{0}")]
|
||||
UnknownError(String),
|
||||
}
|
||||
|
||||
/// A MWixnet server
|
||||
pub trait Server: Send + Sync {
|
||||
/// A public MWixnet server - the "Swap Server"
|
||||
pub trait SwapServer: Send + Sync {
|
||||
/// Submit a new output to be swapped.
|
||||
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>;
|
||||
|
||||
/// Iterate through all saved submissions, filter out any inputs that are no longer spendable,
|
||||
/// and assemble the coinswap transaction, posting the transaction to the configured node.
|
||||
///
|
||||
/// Currently only a single mix node is used. Milestone 3 will include support for multiple mix nodes.
|
||||
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
/// The standard MWixnet server implementation
|
||||
#[derive(Clone)]
|
||||
pub struct ServerImpl {
|
||||
pub struct SwapServerImpl {
|
||||
server_config: ServerConfig,
|
||||
next_server: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
store: Arc<Mutex<SwapStore>>,
|
||||
}
|
||||
|
||||
impl ServerImpl {
|
||||
impl SwapServerImpl {
|
||||
/// Create a new MWixnet server
|
||||
pub fn new(
|
||||
server_config: ServerConfig,
|
||||
next_server: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
store: SwapStore,
|
||||
) -> Self {
|
||||
ServerImpl {
|
||||
SwapServerImpl {
|
||||
server_config,
|
||||
next_server,
|
||||
wallet,
|
||||
node,
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
|
@ -81,20 +89,20 @@ impl ServerImpl {
|
|||
}
|
||||
|
||||
/// Minimum fee to perform a swap.
|
||||
/// Requires enough fee for the mwixnet server's kernel, 1 input and its output to swap.
|
||||
/// Requires enough fee for the swap server's kernel, 1 input and its output to swap.
|
||||
fn get_minimum_swap_fee(&self) -> u64 {
|
||||
TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base()
|
||||
}
|
||||
}
|
||||
|
||||
impl Server for ServerImpl {
|
||||
impl SwapServer for SwapServerImpl {
|
||||
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
// milestone 3: check that enc_payloads length matches number of configured servers
|
||||
if onion.enc_payloads.len() != 1 {
|
||||
return Err(SwapError::InvalidPayloadLength {
|
||||
expected: 1,
|
||||
found: onion.enc_payloads.len(),
|
||||
});
|
||||
// Verify that more than 1 payload exists when there's a next server,
|
||||
// or that exactly 1 payload exists when this is the final server
|
||||
if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1
|
||||
|| self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1
|
||||
{
|
||||
return Err(SwapError::InvalidPayloadLength);
|
||||
}
|
||||
|
||||
// Verify commitment signature to ensure caller owns the output
|
||||
|
@ -112,12 +120,13 @@ impl Server for ServerImpl {
|
|||
commit: onion.commit.clone(),
|
||||
})?;
|
||||
|
||||
// Peel off top layer of encryption
|
||||
let peeled = onion
|
||||
.peel_layer(&self.server_config.key)
|
||||
.map_err(|e| SwapError::PeelOnionFailure(e))?;
|
||||
|
||||
// Verify the fee meets the minimum
|
||||
let fee: u64 = peeled.0.fee.into();
|
||||
let fee: u64 = peeled.payload.fee.into();
|
||||
if fee < self.get_minimum_swap_fee() {
|
||||
return Err(SwapError::FeeTooLow {
|
||||
minimum_fee: self.get_minimum_swap_fee(),
|
||||
|
@ -125,13 +134,13 @@ impl Server for ServerImpl {
|
|||
});
|
||||
}
|
||||
|
||||
// Verify the bullet proof and build the final output
|
||||
if let Some(r) = peeled.0.rangeproof {
|
||||
// Verify the rangeproof
|
||||
if let Some(r) = peeled.payload.rangeproof {
|
||||
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
|
||||
secp.verify_bullet_proof(peeled.1.commit, r, None)
|
||||
secp.verify_bullet_proof(peeled.onion.commit, r, None)
|
||||
.map_err(|_| SwapError::InvalidRangeproof)?;
|
||||
} else {
|
||||
// milestone 3: only the last hop will have a rangeproof
|
||||
} else if peeled.onion.enc_payloads.is_empty() {
|
||||
// A rangeproof is required in the last payload
|
||||
return Err(SwapError::MissingRangeproof);
|
||||
}
|
||||
|
||||
|
@ -140,12 +149,12 @@ impl Server for ServerImpl {
|
|||
locked
|
||||
.save_swap(
|
||||
&SwapData {
|
||||
excess: peeled.0.excess,
|
||||
output_commit: peeled.1.commit,
|
||||
rangeproof: peeled.0.rangeproof,
|
||||
excess: peeled.payload.excess,
|
||||
output_commit: peeled.onion.commit,
|
||||
rangeproof: peeled.payload.rangeproof,
|
||||
input,
|
||||
fee,
|
||||
onion: peeled.1,
|
||||
fee: fee as u64,
|
||||
onion: peeled.onion,
|
||||
status: SwapStatus::Unprocessed,
|
||||
},
|
||||
false,
|
||||
|
@ -174,20 +183,42 @@ impl Server for ServerImpl {
|
|||
node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false)
|
||||
})
|
||||
.filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true))
|
||||
.sorted_by(|a, b| a.output_commit.partial_cmp(&b.output_commit).unwrap())
|
||||
.collect();
|
||||
|
||||
if spendable.len() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum();
|
||||
let (filtered, failed, offset, outputs, kernels) = if let Some(client) = &self.next_server {
|
||||
// Call next mix server
|
||||
let onions = spendable.iter().map(|s| s.onion.clone()).collect();
|
||||
let (indices, mixed) = client
|
||||
.mix_outputs(&onions)
|
||||
.map_err(|e| SwapError::ClientError(e.to_string()))?;
|
||||
|
||||
let inputs: Vec<Input> = spendable.iter().enumerate().map(|(_, s)| s.input).collect();
|
||||
|
||||
let outputs: Vec<Output> = spendable
|
||||
// Filter out failed entries
|
||||
let kept_indices = HashSet::<_>::from_iter(indices.clone());
|
||||
let filtered = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(_, s)| {
|
||||
.filter(|(i, _)| kept_indices.contains(i))
|
||||
.map(|(_, j)| j.clone())
|
||||
.collect();
|
||||
|
||||
let failed = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| !kept_indices.contains(i))
|
||||
.map(|(_, j)| j.clone())
|
||||
.collect();
|
||||
|
||||
(filtered, failed, mixed.offset, mixed.outputs, mixed.kernels)
|
||||
} else {
|
||||
// Build plain outputs for each swap entry
|
||||
let outputs: Vec<Output> = spendable
|
||||
.iter()
|
||||
.map(|s| {
|
||||
Output::new(
|
||||
OutputFeatures::Plain,
|
||||
s.output_commit,
|
||||
|
@ -196,50 +227,59 @@ impl Server for ServerImpl {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let excesses: Vec<SecretKey> = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(_, s)| s.excess.clone())
|
||||
.collect();
|
||||
(spendable, Vec::new(), ZERO_KEY, outputs, Vec::new())
|
||||
};
|
||||
|
||||
let tx = wallet::assemble_tx(
|
||||
let fees_paid: u64 = filtered.iter().map(|s| s.fee).sum();
|
||||
let inputs: Vec<Input> = filtered.iter().map(|s| s.input).collect();
|
||||
let output_excesses: Vec<SecretKey> = filtered.iter().map(|s| s.excess.clone()).collect();
|
||||
|
||||
let tx = tx::assemble_tx(
|
||||
&self.wallet,
|
||||
&inputs,
|
||||
&outputs,
|
||||
&kernels,
|
||||
self.get_fee_base(),
|
||||
total_fee,
|
||||
&excesses,
|
||||
fees_paid,
|
||||
&offset,
|
||||
&output_excesses,
|
||||
)?;
|
||||
|
||||
self.node.post_tx(&tx)?;
|
||||
|
||||
// Update status to in process
|
||||
let kernel_hash = tx.kernels().first().unwrap().hash();
|
||||
for mut swap in spendable {
|
||||
for mut swap in filtered {
|
||||
swap.status = SwapStatus::InProcess { kernel_hash };
|
||||
locked_store.save_swap(&swap, true)?;
|
||||
}
|
||||
|
||||
// Update status of failed swaps
|
||||
for mut swap in failed {
|
||||
swap.status = SwapStatus::Failed;
|
||||
locked_store.save_swap(&swap, true)?;
|
||||
}
|
||||
|
||||
Ok(Some(tx))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{Server, SwapError};
|
||||
use crate::onion::Onion;
|
||||
use crate::secp::ComSignature;
|
||||
use super::{SwapError, SwapServer};
|
||||
use crate::crypto::comsig::ComSignature;
|
||||
|
||||
use grin_core::core::Transaction;
|
||||
use grin_onion::onion::Onion;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct MockServer {
|
||||
pub struct MockSwapServer {
|
||||
errors: HashMap<Onion, SwapError>,
|
||||
}
|
||||
|
||||
impl MockServer {
|
||||
pub fn new() -> MockServer {
|
||||
MockServer {
|
||||
impl MockSwapServer {
|
||||
pub fn new() -> MockSwapServer {
|
||||
MockSwapServer {
|
||||
errors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
@ -249,7 +289,7 @@ pub mod mock {
|
|||
}
|
||||
}
|
||||
|
||||
impl Server for MockServer {
|
||||
impl SwapServer for MockSwapServer {
|
||||
fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
if let Some(e) = self.errors.get(&onion) {
|
||||
return Err(e.clone());
|
||||
|
@ -265,24 +305,57 @@ pub mod mock {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::ServerConfig;
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::onion::test_util::{self, Hop};
|
||||
use crate::onion::Onion;
|
||||
use crate::secp::{self, ComSignature, Commitment, RangeProof, Secp256k1, SecretKey};
|
||||
use crate::server::{Server, ServerImpl, SwapError};
|
||||
use crate::store::{SwapData, SwapStatus, SwapStore};
|
||||
use crate::types::Payload;
|
||||
pub mod test_util {
|
||||
use crate::crypto::dalek::DalekPublicKey;
|
||||
use crate::crypto::secp::SecretKey;
|
||||
use crate::servers::swap::SwapServerImpl;
|
||||
use crate::wallet::mock::MockWallet;
|
||||
use crate::{config, GrinNode, MixClient, SwapStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn new_swapper(
|
||||
test_dir: &str,
|
||||
server_key: &SecretKey,
|
||||
next_server: Option<(&DalekPublicKey, &Arc<dyn MixClient>)>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
) -> (Arc<SwapServerImpl>, Arc<MockWallet>) {
|
||||
let config =
|
||||
config::test_util::local_config(&server_key, &None, &next_server.map(|n| n.0.clone()))
|
||||
.unwrap();
|
||||
|
||||
let wallet = Arc::new(MockWallet::new());
|
||||
let store = SwapStore::new(test_dir).unwrap();
|
||||
let swap_server = Arc::new(SwapServerImpl::new(
|
||||
config,
|
||||
next_server.map(|n| n.1.clone()),
|
||||
wallet.clone(),
|
||||
node,
|
||||
store,
|
||||
));
|
||||
|
||||
(swap_server, wallet)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::servers::swap::{SwapError, SwapServer};
|
||||
use crate::store::{SwapData, SwapStatus};
|
||||
use crate::tx::TxComponents;
|
||||
use crate::{client, tx, MixClient};
|
||||
|
||||
use ::function_name::named;
|
||||
use grin_core::core::hash::Hashed;
|
||||
use grin_core::core::{Committed, FeeFields, Input, OutputFeatures, Transaction, Weighting};
|
||||
use grin_core::global::{self, ChainTypes};
|
||||
use std::net::TcpListener;
|
||||
use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting};
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::crypto::secp;
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_onion::test_util as onion_test_util;
|
||||
use grin_onion::{create_onion, new_hop, Hop};
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use std::sync::Arc;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
use x25519_dalek::StaticSecret;
|
||||
|
||||
macro_rules! assert_error_type {
|
||||
($result:expr, $error_type:pat) => {
|
||||
|
@ -295,103 +368,49 @@ mod tests {
|
|||
};
|
||||
}
|
||||
|
||||
fn new_server(
|
||||
test_name: &str,
|
||||
server_key: &SecretKey,
|
||||
utxos: &Vec<&Commitment>,
|
||||
) -> (ServerImpl, Arc<MockGrinNode>) {
|
||||
global::set_local_chain_type(ChainTypes::AutomatedTesting);
|
||||
let db_root = format!("./target/tmp/.{}", test_name);
|
||||
let _ = std::fs::remove_dir_all(db_root.as_str());
|
||||
|
||||
let config = ServerConfig {
|
||||
key: server_key.clone(),
|
||||
interval_s: 1,
|
||||
addr: TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap(),
|
||||
grin_node_url: "127.0.0.1:3413".parse().unwrap(),
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: "127.0.0.1:3420".parse().unwrap(),
|
||||
wallet_owner_secret_path: None,
|
||||
};
|
||||
let wallet = Arc::new(MockWallet {});
|
||||
let mut mut_node = MockGrinNode::new();
|
||||
for utxo in utxos {
|
||||
mut_node.add_default_utxo(&utxo);
|
||||
}
|
||||
let node = Arc::new(mut_node);
|
||||
let store = SwapStore::new(db_root.as_str()).unwrap();
|
||||
|
||||
let server = ServerImpl::new(config, wallet.clone(), node.clone(), store);
|
||||
(server, node)
|
||||
macro_rules! init_test {
|
||||
() => {{
|
||||
grin_core::global::set_local_chain_type(
|
||||
grin_core::global::ChainTypes::AutomatedTesting,
|
||||
);
|
||||
let test_dir = concat!("./target/tmp/.", function_name!());
|
||||
let _ = std::fs::remove_dir_all(test_dir);
|
||||
test_dir
|
||||
}};
|
||||
}
|
||||
|
||||
fn proof(value: u64, fee: u64, input_blind: &SecretKey, hop_excess: &SecretKey) -> RangeProof {
|
||||
let secp = Secp256k1::new();
|
||||
let nonce = secp::random_secret();
|
||||
|
||||
let mut blind = input_blind.clone();
|
||||
blind.add_assign(&secp, &hop_excess).unwrap();
|
||||
|
||||
secp.bullet_proof(
|
||||
value - fee,
|
||||
blind.clone(),
|
||||
nonce.clone(),
|
||||
nonce.clone(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn new_hop(
|
||||
server_key: &SecretKey,
|
||||
hop_excess: &SecretKey,
|
||||
fee: u64,
|
||||
proof: Option<RangeProof>,
|
||||
) -> Hop {
|
||||
Hop {
|
||||
pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
|
||||
payload: Payload {
|
||||
excess: hop_excess.clone(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: proof,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Single hop to demonstrate request validation and onion unwrapping.
|
||||
/// Standalone swap server to demonstrate request validation and onion unwrapping.
|
||||
#[test]
|
||||
fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[named]
|
||||
fn swap_standalone() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let proof = proof(value, fee, &blind, &hop_excess);
|
||||
let (output_commit, proof) = onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop.clone()])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, node) = new_server("swap_lifecycle", &server_key, &vec![&input_commit]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
server.swap(&onion, &comsig)?;
|
||||
|
||||
// Make sure entry is added to server.
|
||||
let output_commit = secp::add_excess(&input_commit, &hop_excess)?;
|
||||
let output_commit = secp::sub_value(&output_commit, fee)?;
|
||||
|
||||
let expected = SwapData {
|
||||
excess: hop_excess.clone(),
|
||||
output_commit: output_commit.clone(),
|
||||
rangeproof: Some(proof),
|
||||
input: Input::new(OutputFeatures::Plain, input_commit.clone()),
|
||||
fee,
|
||||
fee: fee as u64,
|
||||
onion: Onion {
|
||||
ephemeral_pubkey: test_util::next_ephemeral_pubkey(&onion, &server_key)?,
|
||||
ephemeral_pubkey: xPublicKey::from([0u8; 32]),
|
||||
commit: output_commit.clone(),
|
||||
enc_payloads: vec![],
|
||||
},
|
||||
|
@ -431,33 +450,107 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Multi-server test to verify proper MixClient communication.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_multiserver() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
// Setup input
|
||||
let value: u64 = 200_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
|
||||
// Swapper data
|
||||
let swap_fee: u32 = 50_000_000;
|
||||
let (swap_sk, _swap_pk) = onion_test_util::rand_keypair();
|
||||
let swap_hop_excess = secp::random_secret();
|
||||
let swap_hop = new_hop(&swap_sk, &swap_hop_excess, swap_fee, None);
|
||||
|
||||
// Mixer data
|
||||
let mixer_fee: u32 = 30_000_000;
|
||||
let (mixer_sk, mixer_pk) = onion_test_util::rand_keypair();
|
||||
let mixer_hop_excess = secp::random_secret();
|
||||
let (output_commit, proof) = onion_test_util::proof(
|
||||
value,
|
||||
swap_fee + mixer_fee,
|
||||
&blind,
|
||||
&vec![&swap_hop_excess, &mixer_hop_excess],
|
||||
);
|
||||
let mixer_hop = new_hop(&mixer_sk, &mixer_hop_excess, mixer_fee, Some(proof));
|
||||
|
||||
// Create onion
|
||||
let onion = create_onion(&input_commit, &vec![swap_hop, mixer_hop])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
// Mock mixer
|
||||
let mixer_onion = onion.peel_layer(&swap_sk)?.onion;
|
||||
let mut mock_mixer = client::mock::MockMixClient::new();
|
||||
let mixer_response = TxComponents {
|
||||
offset: ZERO_KEY,
|
||||
outputs: vec![Output::new(
|
||||
OutputFeatures::Plain,
|
||||
output_commit.clone(),
|
||||
proof.clone(),
|
||||
)],
|
||||
kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee as u64)?],
|
||||
};
|
||||
mock_mixer.set_response(
|
||||
&vec![mixer_onion.clone()],
|
||||
(vec![0 as usize], mixer_response),
|
||||
);
|
||||
|
||||
let mixer: Arc<dyn MixClient> = Arc::new(mock_mixer);
|
||||
let (swapper, _) = super::test_util::new_swapper(
|
||||
&test_dir,
|
||||
&swap_sk,
|
||||
Some((&mixer_pk, &mixer)),
|
||||
node.clone(),
|
||||
);
|
||||
swapper.swap(&onion, &comsig)?;
|
||||
|
||||
let tx = swapper.execute_round()?;
|
||||
assert!(tx.is_some());
|
||||
|
||||
// check that the transaction was posted
|
||||
let posted_txns = node.get_posted_txns();
|
||||
assert_eq!(posted_txns.len(), 1);
|
||||
let posted_txn: Transaction = posted_txns.into_iter().next().unwrap();
|
||||
assert!(posted_txn.inputs_committed().contains(&input_commit));
|
||||
assert!(posted_txn.outputs_committed().contains(&output_commit));
|
||||
// todo: check that outputs also contain the commitment generated by our wallet
|
||||
|
||||
posted_txn.validate(Weighting::AsTransaction)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns InvalidPayloadLength when too many payloads are provided.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_too_many_payloads() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let proof = proof(value, fee, &blind, &hop_excess);
|
||||
let (_output_commit, proof) =
|
||||
onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let hops: Vec<Hop> = vec![hop.clone(), hop.clone()]; // Multiple payloads
|
||||
let onion = test_util::create_onion(&input_commit, &hops)?;
|
||||
let onion = create_onion(&input_commit, &hops)?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) =
|
||||
new_server("swap_too_many_payloads", &server_key, &vec![&input_commit]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
assert_eq!(
|
||||
Err(SwapError::InvalidPayloadLength {
|
||||
expected: 1,
|
||||
found: 2
|
||||
}),
|
||||
result
|
||||
);
|
||||
assert_eq!(Err(SwapError::InvalidPayloadLength), result);
|
||||
|
||||
// Make sure no entry is added to the store
|
||||
assert_eq!(
|
||||
|
@ -470,27 +563,28 @@ mod tests {
|
|||
|
||||
/// Returns InvalidComSignature when ComSignature fails to verify.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_invalid_com_signature() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let proof = proof(value, fee, &blind, &hop_excess);
|
||||
let (_output_commit, proof) =
|
||||
onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop])?;
|
||||
|
||||
let wrong_blind = secp::random_secret();
|
||||
let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) = new_server(
|
||||
"swap_invalid_com_signature",
|
||||
&server_key,
|
||||
&vec![&input_commit],
|
||||
);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
assert_eq!(Err(SwapError::InvalidComSignature), result);
|
||||
|
||||
|
@ -505,23 +599,27 @@ mod tests {
|
|||
|
||||
/// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_invalid_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let wrong_value = value + 10_000_000;
|
||||
let proof = proof(wrong_value, fee, &blind, &hop_excess);
|
||||
let (_output_commit, proof) =
|
||||
onion_test_util::proof(wrong_value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) =
|
||||
new_server("swap_invalid_rangeproof", &server_key, &vec![&input_commit]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
assert_eq!(Err(SwapError::InvalidRangeproof), result);
|
||||
|
||||
|
@ -536,9 +634,12 @@ mod tests {
|
|||
|
||||
/// Returns MissingRangeproof when no rangeproof is provided.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_missing_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
|
@ -546,11 +647,11 @@ mod tests {
|
|||
let hop_excess = secp::random_secret();
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, None);
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) =
|
||||
new_server("swap_missing_rangeproof", &server_key, &vec![&input_commit]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
assert_eq!(Err(SwapError::MissingRangeproof), result);
|
||||
|
||||
|
@ -565,21 +666,26 @@ mod tests {
|
|||
|
||||
/// Returns CoinNotFound when there's no matching output in the UTXO set.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let proof = proof(value, fee, &blind, &hop_excess);
|
||||
let (_output_commit, proof) =
|
||||
onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) = new_server("swap_utxo_missing", &server_key, &vec![]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new());
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
assert_eq!(
|
||||
Err(SwapError::CoinNotFound {
|
||||
|
@ -599,21 +705,26 @@ mod tests {
|
|||
|
||||
/// Returns AlreadySwapped when trying to swap the same commitment multiple times.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_already_swapped() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let proof = proof(value, fee, &blind, &hop_excess);
|
||||
let (_output_commit, proof) =
|
||||
onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) = new_server("swap_already_swapped", &server_key, &vec![&input_commit]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
server.swap(&onion, &comsig)?;
|
||||
|
||||
// Call swap a second time
|
||||
|
@ -630,24 +741,28 @@ mod tests {
|
|||
|
||||
/// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_peel_onion_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let fee: u32 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let proof = proof(value, fee, &blind, &hop_excess);
|
||||
let (_output_commit, proof) =
|
||||
onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
|
||||
let wrong_server_key = secp::random_secret();
|
||||
let hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) =
|
||||
new_server("swap_peel_onion_failure", &server_key, &vec![&input_commit]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
|
||||
assert!(result.is_err());
|
||||
|
@ -658,26 +773,31 @@ mod tests {
|
|||
|
||||
/// Returns FeeTooLow when the minimum fee is not met.
|
||||
#[test]
|
||||
#[named]
|
||||
fn swap_fee_too_low() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let test_dir = init_test!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 1_000_000;
|
||||
let fee: u32 = 1_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
|
||||
let server_key = secp::random_secret();
|
||||
let hop_excess = secp::random_secret();
|
||||
let proof = proof(value, fee, &blind, &hop_excess);
|
||||
let (_output_commit, proof) =
|
||||
onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = create_onion(&input_commit, &vec![hop])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let (server, _node) = new_server("swap_fee_too_low", &server_key, &vec![&input_commit]);
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
|
||||
let result = server.swap(&onion, &comsig);
|
||||
assert_eq!(
|
||||
Err(SwapError::FeeTooLow {
|
||||
minimum_fee: 12_500_000,
|
||||
actual_fee: fee
|
||||
actual_fee: fee as u64
|
||||
}),
|
||||
result
|
||||
);
|
|
@ -1,11 +1,12 @@
|
|||
use crate::client::MixClient;
|
||||
use crate::config::ServerConfig;
|
||||
use crate::node::GrinNode;
|
||||
use crate::onion::Onion;
|
||||
use crate::secp::{self, ComSignature};
|
||||
use crate::server::{Server, ServerImpl, SwapError};
|
||||
use crate::servers::swap::{SwapError, SwapServer, SwapServerImpl};
|
||||
use crate::store::SwapStore;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_onion::crypto::comsig::{self, ComSignature};
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_util::StopState;
|
||||
use jsonrpc_core::Value;
|
||||
use jsonrpc_derive::rpc;
|
||||
|
@ -19,31 +20,27 @@ use std::time::Duration;
|
|||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SwapReq {
|
||||
onion: Onion,
|
||||
#[serde(with = "secp::comsig_serde")]
|
||||
#[serde(with = "comsig::comsig_serde")]
|
||||
comsig: ComSignature,
|
||||
}
|
||||
|
||||
#[rpc(server)]
|
||||
pub trait API {
|
||||
pub trait SwapAPI {
|
||||
#[rpc(name = "swap")]
|
||||
fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value>;
|
||||
|
||||
// milestone 3: Used by mwixnet coinswap servers to communicate with each other
|
||||
// fn derive_outputs(&self, entries: Vec<Onion>) -> jsonrpc_core::Result<Value>;
|
||||
// fn derive_kernel(&self, tx: Tx) -> jsonrpc_core::Result<Value>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RPCServer {
|
||||
struct RPCSwapServer {
|
||||
server_config: ServerConfig,
|
||||
server: Arc<Mutex<dyn Server>>,
|
||||
server: Arc<Mutex<dyn SwapServer>>,
|
||||
}
|
||||
|
||||
impl RPCServer {
|
||||
impl RPCSwapServer {
|
||||
/// Spin up an instance of the JSON-RPC HTTP server.
|
||||
fn start_http(&self) -> jsonrpc_http_server::Server {
|
||||
let mut io = IoHandler::new();
|
||||
io.extend_with(RPCServer::to_delegate(self.clone()));
|
||||
io.extend_with(RPCSwapServer::to_delegate(self.clone()));
|
||||
|
||||
ServerBuilder::new(io)
|
||||
.cors(DomainsValidation::Disabled)
|
||||
|
@ -72,8 +69,7 @@ impl From<SwapError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl API for RPCServer {
|
||||
/// Implements the 'swap' API
|
||||
impl SwapAPI for RPCSwapServer {
|
||||
fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value> {
|
||||
self.server
|
||||
.lock()
|
||||
|
@ -86,15 +82,22 @@ impl API for RPCServer {
|
|||
/// Spin up the JSON-RPC web server
|
||||
pub fn listen(
|
||||
server_config: ServerConfig,
|
||||
next_server: Option<Arc<dyn MixClient>>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
store: SwapStore,
|
||||
stop_state: Arc<StopState>,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
let server = ServerImpl::new(server_config.clone(), wallet.clone(), node.clone(), store);
|
||||
let server = SwapServerImpl::new(
|
||||
server_config.clone(),
|
||||
next_server,
|
||||
wallet.clone(),
|
||||
node.clone(),
|
||||
store,
|
||||
);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
let rpc_server = RPCServer {
|
||||
let rpc_server = RPCSwapServer {
|
||||
server_config: server_config.clone(),
|
||||
server: server.clone(),
|
||||
};
|
||||
|
@ -128,12 +131,13 @@ pub fn listen(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::ServerConfig;
|
||||
use crate::onion::test_util;
|
||||
use crate::rpc::{RPCServer, SwapReq};
|
||||
use crate::secp::{self, ComSignature};
|
||||
use crate::server::mock::MockServer;
|
||||
use crate::server::{Server, SwapError};
|
||||
use crate::crypto::comsig::ComSignature;
|
||||
use crate::crypto::secp;
|
||||
use crate::servers::swap::mock::MockSwapServer;
|
||||
use crate::servers::swap::{SwapError, SwapServer};
|
||||
use crate::servers::swap_rpc::{RPCSwapServer, SwapReq};
|
||||
|
||||
use grin_onion::create_onion;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
@ -147,20 +151,23 @@ mod tests {
|
|||
|
||||
/// Spin up a temporary web service, query the API, then cleanup and return response
|
||||
fn make_request(
|
||||
server: Arc<Mutex<dyn Server>>,
|
||||
server: Arc<Mutex<dyn SwapServer>>,
|
||||
req: String,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let server_config = ServerConfig {
|
||||
key: secp::random_secret(),
|
||||
interval_s: 1,
|
||||
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
socks_proxy_addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
grin_node_url: "127.0.0.1:3413".parse()?,
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: "127.0.0.1:3420".parse()?,
|
||||
wallet_owner_secret_path: None,
|
||||
prev_server: None,
|
||||
next_server: None,
|
||||
};
|
||||
|
||||
let rpc_server = RPCServer {
|
||||
let rpc_server = RPCSwapServer {
|
||||
server_config: server_config.clone(),
|
||||
server: server.clone(),
|
||||
};
|
||||
|
@ -201,20 +208,19 @@ mod tests {
|
|||
#[test]
|
||||
fn swap_success() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let commitment = secp::commit(1234, &secp::random_secret())?;
|
||||
let onion = test_util::create_onion(&commitment, &vec![])?;
|
||||
let onion = create_onion(&commitment, &vec![])?;
|
||||
let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?;
|
||||
let swap = SwapReq {
|
||||
onion: onion.clone(),
|
||||
comsig,
|
||||
};
|
||||
|
||||
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(MockServer::new()));
|
||||
let server: Arc<Mutex<dyn SwapServer>> = Arc::new(Mutex::new(MockSwapServer::new()));
|
||||
|
||||
let req = format!(
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||
serde_json::json!(swap)
|
||||
);
|
||||
println!("Request: {}", req);
|
||||
let response = make_request(server, req)?;
|
||||
let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n";
|
||||
assert_eq!(response, expected);
|
||||
|
@ -224,7 +230,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(MockServer::new()));
|
||||
let server: Arc<Mutex<dyn SwapServer>> = Arc::new(Mutex::new(MockSwapServer::new()));
|
||||
|
||||
let params = "{ \"param\": \"Not a valid Swap request\" }";
|
||||
let req = format!(
|
||||
|
@ -241,21 +247,21 @@ mod tests {
|
|||
#[test]
|
||||
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let commitment = secp::commit(1234, &secp::random_secret())?;
|
||||
let onion = test_util::create_onion(&commitment, &vec![])?;
|
||||
let onion = create_onion(&commitment, &vec![])?;
|
||||
let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?;
|
||||
let swap = SwapReq {
|
||||
onion: onion.clone(),
|
||||
comsig,
|
||||
};
|
||||
|
||||
let mut server = MockServer::new();
|
||||
let mut server = MockSwapServer::new();
|
||||
server.set_response(
|
||||
&onion,
|
||||
SwapError::CoinNotFound {
|
||||
commit: commitment.clone(),
|
||||
},
|
||||
);
|
||||
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(server));
|
||||
let server: Arc<Mutex<dyn SwapServer>> = Arc::new(Mutex::new(server));
|
||||
|
||||
let req = format!(
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
33
src/store.rs
33
src/store.rs
|
@ -1,7 +1,7 @@
|
|||
use crate::onion::Onion;
|
||||
use crate::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use crate::types::{read_optional, write_optional};
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_onion::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_onion::util::{read_optional, write_optional};
|
||||
|
||||
use grin_core::core::Input;
|
||||
use grin_core::ser::{
|
||||
|
@ -23,6 +23,7 @@ pub enum SwapStatus {
|
|||
Unprocessed,
|
||||
InProcess { kernel_hash: Hash },
|
||||
Completed { kernel_hash: Hash, block_hash: Hash },
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl Writeable for SwapStatus {
|
||||
|
@ -43,6 +44,9 @@ impl Writeable for SwapStatus {
|
|||
kernel_hash.write(writer)?;
|
||||
block_hash.write(writer)?;
|
||||
}
|
||||
SwapStatus::Failed => {
|
||||
writer.write_u8(3)?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
@ -65,6 +69,7 @@ impl Readable for SwapStatus {
|
|||
block_hash,
|
||||
}
|
||||
}
|
||||
3 => SwapStatus::Failed,
|
||||
_ => {
|
||||
return Err(ser::Error::CorruptedData);
|
||||
}
|
||||
|
@ -239,12 +244,12 @@ impl SwapStore {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::onion::test_util::rand_onion;
|
||||
use crate::secp::test_util::{rand_commit, rand_hash, rand_proof};
|
||||
use crate::store::{SwapData, SwapStatus, SwapStore};
|
||||
use crate::{secp, StoreError};
|
||||
use crate::StoreError;
|
||||
use grin_core::core::{Input, OutputFeatures};
|
||||
use grin_core::global::{self, ChainTypes};
|
||||
use grin_onion::crypto::secp;
|
||||
use grin_onion::test_util as onion_test_util;
|
||||
use rand::RngCore;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
|
@ -258,11 +263,11 @@ mod tests {
|
|||
fn rand_swap_with_status(status: SwapStatus) -> SwapData {
|
||||
SwapData {
|
||||
excess: secp::random_secret(),
|
||||
output_commit: rand_commit(),
|
||||
rangeproof: Some(rand_proof()),
|
||||
input: Input::new(OutputFeatures::Plain, rand_commit()),
|
||||
output_commit: onion_test_util::rand_commit(),
|
||||
rangeproof: Some(onion_test_util::rand_proof()),
|
||||
input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()),
|
||||
fee: rand::thread_rng().next_u64(),
|
||||
onion: rand_onion(),
|
||||
onion: onion_test_util::rand_onion(),
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
@ -273,12 +278,12 @@ mod tests {
|
|||
SwapStatus::Unprocessed
|
||||
} else if s == 1 {
|
||||
SwapStatus::InProcess {
|
||||
kernel_hash: rand_hash(),
|
||||
kernel_hash: onion_test_util::rand_hash(),
|
||||
}
|
||||
} else {
|
||||
SwapStatus::Completed {
|
||||
kernel_hash: rand_hash(),
|
||||
block_hash: rand_hash(),
|
||||
kernel_hash: onion_test_util::rand_hash(),
|
||||
block_hash: onion_test_util::rand_hash(),
|
||||
}
|
||||
};
|
||||
rand_swap_with_status(status)
|
||||
|
@ -325,7 +330,7 @@ mod tests {
|
|||
assert!(store.swap_exists(&swap.input.commit)?);
|
||||
|
||||
swap.status = SwapStatus::InProcess {
|
||||
kernel_hash: rand_hash(),
|
||||
kernel_hash: onion_test_util::rand_hash(),
|
||||
};
|
||||
let result = store.save_swap(&swap, false);
|
||||
assert_eq!(
|
||||
|
|
83
src/tor.rs
Normal file
83
src/tor.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use crate::config::{self, ServerConfig};
|
||||
|
||||
use grin_core::global;
|
||||
use grin_wallet_impls::tor::config as tor_config;
|
||||
use grin_wallet_impls::tor::process::TorProcess;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Tor error types
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TorError {
|
||||
#[error("Error generating config: {0:?}")]
|
||||
ConfigError(String),
|
||||
#[error("Error starting process: {0:?}")]
|
||||
ProcessError(grin_wallet_impls::tor::process::Error),
|
||||
}
|
||||
|
||||
pub fn init_tor_listener(server_config: &ServerConfig) -> Result<TorProcess, TorError> {
|
||||
println!("Initializing tor listener");
|
||||
|
||||
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
|
||||
tor_dir.push("tor/listener");
|
||||
|
||||
let mut torrc_dir = tor_dir.clone();
|
||||
torrc_dir.push("torrc");
|
||||
|
||||
tor_config::output_tor_listener_config(
|
||||
tor_dir.to_str().unwrap(),
|
||||
server_config.addr.to_string().as_str(),
|
||||
&vec![server_config.key.clone()],
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
)
|
||||
.map_err(|e| TorError::ConfigError(e.to_string()))?;
|
||||
|
||||
// Start TOR process
|
||||
let mut process = TorProcess::new();
|
||||
process
|
||||
.torrc_path(torrc_dir.to_str().unwrap())
|
||||
.working_dir(tor_dir.to_str().unwrap())
|
||||
.timeout(20)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.map_err(TorError::ProcessError)?;
|
||||
|
||||
println!(
|
||||
"Server listening at http://{}.onion",
|
||||
server_config.onion_address().to_ov3_str()
|
||||
);
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
pub fn init_tor_sender(server_config: &ServerConfig) -> Result<TorProcess, TorError> {
|
||||
println!(
|
||||
"Starting TOR Process for send at {:?}",
|
||||
server_config.socks_proxy_addr
|
||||
);
|
||||
|
||||
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
|
||||
tor_dir.push("tor/sender");
|
||||
|
||||
let mut torrc_dir = tor_dir.clone();
|
||||
torrc_dir.push("torrc");
|
||||
|
||||
tor_config::output_tor_sender_config(
|
||||
tor_dir.to_str().unwrap(),
|
||||
&server_config.socks_proxy_addr.to_string(),
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
)
|
||||
.map_err(|e| TorError::ConfigError(e.to_string()))?;
|
||||
|
||||
// Start TOR process
|
||||
let mut tor_process = TorProcess::new();
|
||||
tor_process
|
||||
.torrc_path(torrc_dir.to_str().unwrap())
|
||||
.working_dir(tor_dir.to_str().unwrap())
|
||||
.timeout(20)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.map_err(TorError::ProcessError)?;
|
||||
Ok(tor_process)
|
||||
}
|
223
src/tx.rs
Normal file
223
src/tx.rs
Normal file
|
@ -0,0 +1,223 @@
|
|||
use crate::crypto::secp;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
use grin_core::core::{
|
||||
FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel,
|
||||
};
|
||||
use grin_keychain::BlindingFactor;
|
||||
use secp256k1zkp::{ContextFlag, Secp256k1, SecretKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error types for interacting with wallets
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TxError {
|
||||
#[error("Error computing transactions's offset: {0:?}")]
|
||||
OffsetError(secp256k1zkp::Error),
|
||||
#[error("Error building kernel's fee fields: {0:?}")]
|
||||
KernelFeeError(grin_core::core::transaction::Error),
|
||||
#[error("Error computing kernel's excess: {0:?}")]
|
||||
KernelExcessError(secp256k1zkp::Error),
|
||||
#[error("Error computing kernel's signature message: {0:?}")]
|
||||
KernelSigMessageError(grin_core::core::transaction::Error),
|
||||
#[error("Error signing kernel: {0:?}")]
|
||||
KernelSigError(secp256k1zkp::Error),
|
||||
#[error("Built kernel failed to verify: {0:?}")]
|
||||
KernelVerifyError(grin_core::core::transaction::Error),
|
||||
#[error("Output blinding factor is invalid: {0:?}")]
|
||||
OutputBlindError(secp256k1zkp::Error),
|
||||
#[error("Wallet error: {0:?}")]
|
||||
WalletError(crate::wallet::WalletError),
|
||||
}
|
||||
|
||||
/// A collection of transaction components
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TxComponents {
|
||||
/// Transaction offset
|
||||
pub offset: SecretKey,
|
||||
/// Transaction kernels
|
||||
pub kernels: Vec<TxKernel>,
|
||||
/// Transaction outputs
|
||||
pub outputs: Vec<Output>,
|
||||
}
|
||||
|
||||
/// Builds and verifies the finalized swap 'Transaction' using the provided components.
|
||||
pub fn assemble_tx(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
inputs: &Vec<Input>,
|
||||
outputs: &Vec<Output>,
|
||||
kernels: &Vec<TxKernel>,
|
||||
fee_base: u64,
|
||||
fees_paid: u64,
|
||||
prev_offset: &SecretKey,
|
||||
output_excesses: &Vec<SecretKey>,
|
||||
) -> Result<Transaction, TxError> {
|
||||
// calculate minimum fee required for the kernel
|
||||
let min_kernel_fee =
|
||||
TransactionBody::weight_by_iok(inputs.len() as u64, outputs.len() as u64, 1) * fee_base;
|
||||
|
||||
let components = add_kernel_and_collect_fees(
|
||||
&wallet,
|
||||
&outputs,
|
||||
&kernels,
|
||||
fee_base,
|
||||
min_kernel_fee,
|
||||
fees_paid,
|
||||
&prev_offset,
|
||||
&output_excesses,
|
||||
)?;
|
||||
|
||||
// assemble the transaction
|
||||
let tx = Transaction::new(
|
||||
Inputs::from(inputs.as_slice()),
|
||||
&components.outputs,
|
||||
&components.kernels,
|
||||
)
|
||||
.with_offset(BlindingFactor::from_secret_key(components.offset));
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
/// Adds a kernel and output to a collection of transaction components to consume fees and offset excesses.
|
||||
pub fn assemble_components(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
components: &TxComponents,
|
||||
output_excesses: &Vec<SecretKey>,
|
||||
fee_base: u64,
|
||||
fees_paid: u64,
|
||||
) -> Result<TxComponents, TxError> {
|
||||
// calculate minimum fee required for the kernel
|
||||
let min_kernel_fee = TransactionBody::weight_by_iok(0, 0, 1) * fee_base;
|
||||
|
||||
add_kernel_and_collect_fees(
|
||||
&wallet,
|
||||
&components.outputs,
|
||||
&components.kernels,
|
||||
fee_base,
|
||||
min_kernel_fee,
|
||||
fees_paid,
|
||||
&components.offset,
|
||||
&output_excesses,
|
||||
)
|
||||
}
|
||||
|
||||
fn add_kernel_and_collect_fees(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
outputs: &Vec<Output>,
|
||||
kernels: &Vec<TxKernel>,
|
||||
fee_base: u64,
|
||||
min_kernel_fee: u64,
|
||||
fees_paid: u64,
|
||||
prev_offset: &SecretKey,
|
||||
output_excesses: &Vec<SecretKey>,
|
||||
) -> Result<TxComponents, TxError> {
|
||||
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||
let mut txn_outputs = outputs.clone();
|
||||
let mut txn_excesses = output_excesses.clone();
|
||||
let mut txn_kernels = kernels.clone();
|
||||
let mut kernel_fee = fees_paid;
|
||||
|
||||
// calculate fee required if we add our own output
|
||||
let fee_to_collect = TransactionBody::weight_by_iok(0, 1, 0) * fee_base;
|
||||
|
||||
// calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it
|
||||
let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base;
|
||||
|
||||
// collect any leftover fees
|
||||
if fees_paid > min_kernel_fee + fee_to_collect + fee_to_spend {
|
||||
let amount = fees_paid - (min_kernel_fee + fee_to_collect);
|
||||
kernel_fee -= amount;
|
||||
|
||||
let wallet_output = wallet.build_output(amount).map_err(TxError::WalletError)?;
|
||||
txn_outputs.push(wallet_output.1);
|
||||
|
||||
let output_excess = SecretKey::from_slice(&secp, &wallet_output.0.as_ref())
|
||||
.map_err(TxError::OutputBlindError)?;
|
||||
txn_excesses.push(output_excess);
|
||||
}
|
||||
|
||||
// generate random transaction offset
|
||||
let our_offset = secp::random_secret();
|
||||
let txn_offset = secp
|
||||
.blind_sum(vec![prev_offset.clone(), our_offset.clone()], Vec::new())
|
||||
.map_err(TxError::OffsetError)?;
|
||||
|
||||
// calculate kernel excess
|
||||
let kern_excess = secp
|
||||
.blind_sum(txn_excesses, vec![our_offset.clone()])
|
||||
.map_err(TxError::KernelExcessError)?;
|
||||
|
||||
// build and verify kernel
|
||||
let kernel = build_kernel(&kern_excess, kernel_fee)?;
|
||||
txn_kernels.push(kernel);
|
||||
|
||||
// Sort outputs & kernels by commitment
|
||||
txn_kernels.sort_by(|a, b| a.excess.partial_cmp(&b.excess).unwrap());
|
||||
txn_outputs.sort_by(|a, b| {
|
||||
a.identifier
|
||||
.commit
|
||||
.partial_cmp(&b.identifier.commit)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Ok(TxComponents {
|
||||
offset: txn_offset,
|
||||
kernels: txn_kernels,
|
||||
outputs: txn_outputs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Builds a transaction kernel for the Grin network.
|
||||
///
|
||||
/// Transaction kernels are a critical part of the Grin transaction process. Each transaction contains a
|
||||
/// kernel. It includes features chosen for this transaction, a fee chosen for this transaction, and
|
||||
/// a proof that the total sum of outputs, transaction fees and block reward equals the total sum of inputs.
|
||||
/// The `build_kernel` function handles this process, building the kernel and handling any potential errors.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `excess`: A reference to a `SecretKey`. This key is used as an excess value for the transaction.
|
||||
/// The excess is a kind of cryptographic proof that the total sum of outputs and fees equals the
|
||||
/// total sum of inputs.
|
||||
/// * `fee`: An unsigned 64-bit integer representing the transaction fee in nanogrin. This is the fee
|
||||
/// that will be paid to the miner who mines the block containing this transaction.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The function returns a `Result` enum with `TxKernel` as the Ok variant and `TxError` as the Err variant.
|
||||
/// If the kernel is successfully built, it is returned as part of the Ok variant. If there is an error at any point
|
||||
/// during the process, it is returned as part of the Err variant.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function can return several types of errors, all defined in the `TxError` enum. These include:
|
||||
///
|
||||
/// * `KernelFeeError`: There was an error building the kernel's fee fields.
|
||||
/// * `KernelExcessError`: There was an error computing the kernel's excess.
|
||||
/// * `KernelSigMessageError`: There was an error computing the kernel's signature message.
|
||||
/// * `KernelSigError`: There was an error signing the kernel.
|
||||
/// * `KernelVerifyError`: The built kernel failed to verify.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use secp256k1zkp::key::SecretKey;
|
||||
/// use crate::crypto::secp;
|
||||
///
|
||||
/// let secret_key = SecretKey::new(&mut secp::rand::thread_rng());
|
||||
/// let fee = 10; // 10 nanogrin
|
||||
/// let kernel = build_kernel(&secret_key, fee);
|
||||
/// ```
|
||||
pub fn build_kernel(excess: &SecretKey, fee: u64) -> Result<TxKernel, TxError> {
|
||||
let mut kernel = TxKernel::with_features(KernelFeatures::Plain {
|
||||
fee: FeeFields::new(0, fee).map_err(TxError::KernelFeeError)?,
|
||||
});
|
||||
let msg = kernel
|
||||
.msg_to_sign()
|
||||
.map_err(TxError::KernelSigMessageError)?;
|
||||
kernel.excess = secp::commit(0, &excess).map_err(TxError::KernelExcessError)?;
|
||||
kernel.excess_sig = secp::sign(&excess, &msg).map_err(TxError::KernelSigError)?;
|
||||
kernel.verify().map_err(TxError::KernelVerifyError)?;
|
||||
|
||||
Ok(kernel)
|
||||
}
|
82
src/types.rs
82
src/types.rs
|
@ -1,82 +0,0 @@
|
|||
use crate::secp::{self, RangeProof, SecretKey};
|
||||
|
||||
use grin_core::core::FeeFields;
|
||||
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const CURRENT_VERSION: u8 = 0;
|
||||
|
||||
/// Writes an optional value as '1' + value if Some, or '0' if None
|
||||
pub fn write_optional<O: Writeable, W: Writer>(
|
||||
writer: &mut W,
|
||||
o: &Option<O>,
|
||||
) -> Result<(), ser::Error> {
|
||||
match &o {
|
||||
Some(o) => {
|
||||
writer.write_u8(1)?;
|
||||
o.write(writer)?;
|
||||
}
|
||||
None => writer.write_u8(0)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads an optional value as '1' + value if Some, or '0' if None
|
||||
pub fn read_optional<O: Readable, R: Reader>(reader: &mut R) -> Result<Option<O>, ser::Error> {
|
||||
let o = if reader.read_u8()? == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(O::read(reader)?)
|
||||
};
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
// todo: Belongs in Onion
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Payload {
|
||||
pub excess: SecretKey,
|
||||
pub fee: FeeFields,
|
||||
pub rangeproof: Option<RangeProof>,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
pub fn deserialize(bytes: &Vec<u8>) -> Result<Payload, ser::Error> {
|
||||
let payload: Payload = ser::deserialize_default(&mut &bytes[..])?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> {
|
||||
let mut vec = vec![];
|
||||
ser::serialize_default(&mut vec, &self)?;
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
impl Readable for Payload {
|
||||
fn read<R: Reader>(reader: &mut R) -> Result<Payload, ser::Error> {
|
||||
let version = reader.read_u8()?;
|
||||
if version != CURRENT_VERSION {
|
||||
return Err(ser::Error::UnsupportedProtocolVersion);
|
||||
}
|
||||
|
||||
let excess = secp::read_secret_key(reader)?;
|
||||
let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?;
|
||||
let rangeproof = read_optional(reader)?;
|
||||
Ok(Payload {
|
||||
excess,
|
||||
fee,
|
||||
rangeproof,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Writeable for Payload {
|
||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_u8(CURRENT_VERSION)?;
|
||||
writer.write_fixed_bytes(&self.excess)?;
|
||||
writer.write_u64(self.fee.into())?;
|
||||
write_optional(writer, &self.rangeproof)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
111
src/wallet.rs
111
src/wallet.rs
|
@ -1,19 +1,16 @@
|
|||
use crate::secp;
|
||||
use crate::crypto::secp;
|
||||
|
||||
use grin_api::client;
|
||||
use grin_api::json_rpc::{build_request, Request, Response};
|
||||
use grin_core::core::{
|
||||
FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel,
|
||||
};
|
||||
use grin_core::core::Output;
|
||||
use grin_core::libtx::secp_ser;
|
||||
use grin_keychain::BlindingFactor;
|
||||
use grin_util::{ToHex, ZeroingString};
|
||||
use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token};
|
||||
use secp256k1zkp::{ContextFlag, PublicKey, Secp256k1, SecretKey};
|
||||
use secp256k1zkp::{PublicKey, Secp256k1, SecretKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
pub trait Wallet: Send + Sync {
|
||||
|
@ -24,18 +21,6 @@ pub trait Wallet: Send + Sync {
|
|||
/// Error types for interacting with wallets
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WalletError {
|
||||
#[error("Error building kernel's fee fields: {0:?}")]
|
||||
KernelFeeError(grin_core::core::transaction::Error),
|
||||
#[error("Error computing kernel's excess: {0:?}")]
|
||||
KernelExcessError(secp256k1zkp::Error),
|
||||
#[error("Error computing kernel's signature message: {0:?}")]
|
||||
KernelSigMessageError(grin_core::core::transaction::Error),
|
||||
#[error("Error signing kernel: {0:?}")]
|
||||
KernelSigError(secp256k1zkp::Error),
|
||||
#[error("Built kernel failed to verify: {0:?}")]
|
||||
KernelVerifyError(grin_core::core::transaction::Error),
|
||||
#[error("Output blinding factor is invalid: {0:?}")]
|
||||
OutputBlindError(secp256k1zkp::Error),
|
||||
#[error("Error encrypting request: {0:?}")]
|
||||
EncryptRequestError(grin_wallet_libwallet::Error),
|
||||
#[error("Error decrypting response: {0:?}")]
|
||||
|
@ -48,67 +33,6 @@ pub enum WalletError {
|
|||
ResponseParseError(grin_api::json_rpc::Error),
|
||||
}
|
||||
|
||||
/// Builds and verifies a 'Transaction' using the provided components.
|
||||
pub fn assemble_tx(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
inputs: &Vec<Input>,
|
||||
outputs: &Vec<Output>,
|
||||
fee_base: u64,
|
||||
total_fee: u64,
|
||||
excesses: &Vec<SecretKey>,
|
||||
) -> Result<Transaction, WalletError> {
|
||||
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||
let txn_inputs = Inputs::from(inputs.as_slice());
|
||||
let mut txn_outputs = outputs.clone();
|
||||
let mut txn_excesses = excesses.clone();
|
||||
let mut kernel_fee = total_fee;
|
||||
|
||||
// calculate fee required if we add our own output
|
||||
let fee_required =
|
||||
TransactionBody::weight_by_iok(inputs.len() as u64, (outputs.len() + 1) as u64, 1)
|
||||
* fee_base;
|
||||
|
||||
// calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it
|
||||
let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base;
|
||||
|
||||
// collect any leftover fees
|
||||
if total_fee > fee_required + fee_to_spend {
|
||||
let amount = total_fee - fee_required;
|
||||
kernel_fee -= amount;
|
||||
|
||||
let wallet_output = wallet.build_output(amount)?;
|
||||
txn_outputs.push(wallet_output.1);
|
||||
|
||||
let output_excess = SecretKey::from_slice(&secp, &wallet_output.0.as_ref())
|
||||
.map_err(WalletError::OutputBlindError)?;
|
||||
txn_excesses.push(output_excess);
|
||||
}
|
||||
|
||||
// generate random transaction offset
|
||||
let offset = secp::random_secret();
|
||||
|
||||
// calculate kernel excess
|
||||
let kern_excess = secp
|
||||
.blind_sum(txn_excesses, vec![offset.clone()])
|
||||
.map_err(WalletError::KernelExcessError)?;
|
||||
|
||||
// build and verify kernel
|
||||
let mut kernel = TxKernel::with_features(KernelFeatures::Plain {
|
||||
fee: FeeFields::new(0, kernel_fee).map_err(WalletError::KernelFeeError)?,
|
||||
});
|
||||
let msg = kernel
|
||||
.msg_to_sign()
|
||||
.map_err(WalletError::KernelSigMessageError)?;
|
||||
kernel.excess = secp::commit(0, &kern_excess).map_err(WalletError::KernelExcessError)?;
|
||||
kernel.excess_sig = secp::sign(&kern_excess, &msg).map_err(WalletError::KernelSigError)?;
|
||||
kernel.verify().map_err(WalletError::KernelVerifyError)?;
|
||||
|
||||
// assemble the transaction
|
||||
let tx = Transaction::new(txn_inputs, &txn_outputs, &[kernel])
|
||||
.with_offset(BlindingFactor::from_secret_key(offset));
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
|
||||
#[derive(Clone)]
|
||||
pub struct HttpWallet {
|
||||
|
@ -272,15 +196,34 @@ impl Wallet for HttpWallet {
|
|||
#[cfg(test)]
|
||||
pub mod mock {
|
||||
use super::{Wallet, WalletError};
|
||||
use crate::secp;
|
||||
use crate::crypto::secp;
|
||||
use std::borrow::BorrowMut;
|
||||
|
||||
use grin_core::core::{Output, OutputFeatures};
|
||||
use grin_keychain::BlindingFactor;
|
||||
use secp256k1zkp::pedersen::Commitment;
|
||||
use secp256k1zkp::Secp256k1;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
|
||||
/// Mock implementation of the 'Wallet' trait for unit-tests.
|
||||
#[derive(Clone)]
|
||||
pub struct MockWallet {}
|
||||
pub struct MockWallet {
|
||||
built_outputs: Arc<Mutex<Vec<Commitment>>>,
|
||||
}
|
||||
|
||||
impl MockWallet {
|
||||
/// Creates a new, empty MockWallet.
|
||||
pub fn new() -> Self {
|
||||
MockWallet {
|
||||
built_outputs: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the commitments of all outputs built for the wallet.
|
||||
pub fn built_outputs(&self) -> Vec<Commitment> {
|
||||
self.built_outputs.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Wallet for MockWallet {
|
||||
/// Builds an 'Output' for the wallet using the 'build_output' RPC API.
|
||||
|
@ -297,6 +240,10 @@ pub mod mock {
|
|||
None,
|
||||
);
|
||||
let output = Output::new(OutputFeatures::Plain, commit.clone(), proof);
|
||||
|
||||
let mut locked = self.built_outputs.lock().unwrap();
|
||||
locked.borrow_mut().push(output.commitment().clone());
|
||||
|
||||
Ok((BlindingFactor::from_secret_key(blind), output))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue