mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-01-20 19:11:09 +03:00
generate random pubkey for each payload (https://github.com/mimblewimble/mwixnet/issues/19)
This commit is contained in:
parent
76532c899e
commit
d99aa6ec7c
14 changed files with 695 additions and 262 deletions
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"
|
||||
}
|
||||
```
|
|
@ -7,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,
|
||||
|
|
|
@ -174,7 +174,6 @@ pub mod test_util {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::dalek::test_util;
|
||||
use crate::crypto::dalek::test_util::rand_keypair;
|
||||
use grin_core::ser::{self, ProtocolVersion};
|
||||
use grin_util::ToHex;
|
||||
|
@ -191,7 +190,7 @@ mod tests {
|
|||
#[test]
|
||||
fn pubkey_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test from_hex
|
||||
let rand_pk = test_util::rand_keypair().1;
|
||||
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);
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ pub mod test_util {
|
|||
|
||||
pub fn proof(
|
||||
value: u64,
|
||||
fee: u64,
|
||||
fee: u32,
|
||||
input_blind: &SecretKey,
|
||||
hop_excesses: &Vec<&SecretKey>,
|
||||
) -> (Commitment, RangeProof) {
|
||||
|
@ -101,7 +101,7 @@ pub mod test_util {
|
|||
blind.add_assign(&secp, &hop_excess).unwrap();
|
||||
}
|
||||
|
||||
let out_value = value - fee;
|
||||
let out_value = value - (fee as u64);
|
||||
|
||||
let rp = secp.bullet_proof(
|
||||
out_value,
|
||||
|
|
|
@ -28,7 +28,7 @@ mod servers;
|
|||
mod store;
|
||||
mod tor;
|
||||
mod tx;
|
||||
mod types;
|
||||
mod util;
|
||||
mod wallet;
|
||||
|
||||
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
|
||||
|
|
249
src/onion.rs
249
src/onion.rs
|
@ -1,17 +1,17 @@
|
|||
use crate::crypto::secp::{self, Commitment, SecretKey};
|
||||
use crate::types::Payload;
|
||||
|
||||
use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use crate::onion::OnionError::{InvalidKeyLength, SerializationError};
|
||||
use crate::util::{read_optional, vec_to_array, write_optional};
|
||||
|
||||
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;
|
||||
|
@ -21,6 +21,8 @@ use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret};
|
|||
type HmacSha256 = Hmac<Sha256>;
|
||||
type RawBytes = Vec<u8>;
|
||||
|
||||
const CURRENT_ONION_VERSION: u8 = 0;
|
||||
|
||||
/// A data packet with layers of encryption
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Onion {
|
||||
|
@ -53,8 +55,59 @@ 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)
|
||||
}
|
||||
|
||||
#[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_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
|
||||
|
@ -74,8 +127,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<PeeledOnion, 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();
|
||||
|
@ -95,15 +148,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))?;
|
||||
|
@ -111,7 +155,7 @@ 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,
|
||||
};
|
||||
|
@ -122,22 +166,6 @@ impl 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> {
|
||||
let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?;
|
||||
mu_hmac.update(shared_secret.as_bytes());
|
||||
|
@ -164,8 +192,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();
|
||||
|
@ -236,9 +263,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()?;
|
||||
|
@ -303,10 +330,9 @@ impl From<ser::Error> for OnionError {
|
|||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use super::{Onion, OnionError, RawBytes};
|
||||
use super::{Onion, OnionError, Payload, RawBytes};
|
||||
use crate::crypto::secp::test_util::{rand_commit, rand_proof};
|
||||
use crate::crypto::secp::{random_secret, Commitment, SecretKey};
|
||||
use crate::types::Payload;
|
||||
|
||||
use chacha20::cipher::StreamCipher;
|
||||
use grin_core::core::FeeFields;
|
||||
|
@ -317,54 +343,59 @@ pub mod test_util {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct Hop {
|
||||
pub pubkey: xPublicKey,
|
||||
pub payload: Payload,
|
||||
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: u64,
|
||||
fee: u32,
|
||||
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,
|
||||
},
|
||||
server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
|
||||
excess: hop_excess.clone(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: proof,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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();
|
||||
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();
|
||||
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)?;
|
||||
|
||||
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);
|
||||
enc_payloads.push(hop.payload.serialize()?);
|
||||
ephemeral_key = StaticSecret::from(
|
||||
*ephemeral_key
|
||||
.diffie_hellman(&xPublicKey::from(&blinding_factor))
|
||||
.as_bytes(),
|
||||
);
|
||||
|
||||
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() {
|
||||
|
@ -375,7 +406,7 @@ pub mod test_util {
|
|||
}
|
||||
|
||||
let onion = Onion {
|
||||
ephemeral_pubkey: xPublicKey::from(&initial_key),
|
||||
ephemeral_pubkey: onion_ephemeral_pk,
|
||||
commit: commitment.clone(),
|
||||
enc_payloads,
|
||||
};
|
||||
|
@ -387,70 +418,56 @@ pub mod test_util {
|
|||
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
|
||||
},
|
||||
},
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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())))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::test_util::{self, Hop};
|
||||
use crate::crypto::secp;
|
||||
use crate::types::Payload;
|
||||
use super::test_util::{new_hop, Hop};
|
||||
use super::*;
|
||||
use crate::crypto::secp::random_secret;
|
||||
|
||||
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(),
|
||||
|
@ -465,20 +482,15 @@ 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,
|
||||
};
|
||||
|
@ -489,10 +501,7 @@ pub mod tests {
|
|||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -324,9 +324,12 @@ mod tests {
|
|||
use crate::crypto::secp::{self, Commitment};
|
||||
use crate::node::mock::MockGrinNode;
|
||||
use crate::onion::test_util;
|
||||
use crate::MixClient;
|
||||
use crate::{DalekPublicKey, MixClient};
|
||||
|
||||
use crate::onion::test_util::Hop;
|
||||
use ::function_name::named;
|
||||
use secp256k1zkp::pedersen::RangeProof;
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -341,50 +344,89 @@ mod tests {
|
|||
}};
|
||||
}
|
||||
|
||||
struct ServerVars {
|
||||
fee: u32,
|
||||
sk: SecretKey,
|
||||
pk: DalekPublicKey,
|
||||
excess: SecretKey,
|
||||
}
|
||||
|
||||
impl ServerVars {
|
||||
fn new(fee: u32) -> Self {
|
||||
let (sk, pk) = dalek::test_util::rand_keypair();
|
||||
let excess = secp::random_secret();
|
||||
ServerVars {
|
||||
fee,
|
||||
sk,
|
||||
pk,
|
||||
excess,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hop(&self, proof: Option<RangeProof>) -> Hop {
|
||||
test_util::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!();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let input_commit = secp::commit(value, &blind)?;
|
||||
let node = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
// 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];
|
||||
|
||||
let (swap_sk, swap_pk) = dalek::test_util::rand_keypair();
|
||||
let (mix1_sk, mix1_pk) = dalek::test_util::rand_keypair();
|
||||
let (mix2_sk, mix2_pk) = dalek::test_util::rand_keypair();
|
||||
|
||||
let swap_hop_excess = secp::random_secret();
|
||||
let swap_hop = test_util::new_hop(&swap_sk, &swap_hop_excess, fee, None);
|
||||
|
||||
let mix1_hop_excess = secp::random_secret();
|
||||
let mix1_hop = test_util::new_hop(&mix1_sk, &mix1_hop_excess, fee, None);
|
||||
|
||||
let mix2_hop_excess = secp::random_secret();
|
||||
let (out_commit, proof) = secp::test_util::proof(
|
||||
value,
|
||||
fee * 3,
|
||||
&blind,
|
||||
&vec![&swap_hop_excess, &mix1_hop_excess, &mix2_hop_excess],
|
||||
// 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 mix2_hop = test_util::new_hop(&mix2_sk, &mix2_hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![swap_hop, mix1_hop, mix2_hop])?;
|
||||
|
||||
let (mixer2_client, mixer2_wallet) =
|
||||
super::test_util::new_mixer(&mix2_sk, (&mix1_sk, &mix1_pk), &None, &node);
|
||||
|
||||
let (mixer1_client, mixer1_wallet) = super::test_util::new_mixer(
|
||||
&mix1_sk,
|
||||
(&swap_sk, &swap_pk),
|
||||
&Some((mix2_pk.clone(), mixer2_client.clone())),
|
||||
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,
|
||||
);
|
||||
|
||||
// Emulate the swap server peeling the onion and then calling mix1
|
||||
let mix1_onion = onion.peel_layer(&swap_sk)?;
|
||||
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) = secp::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 = test_util::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()])?;
|
||||
|
||||
|
@ -396,7 +438,7 @@ mod tests {
|
|||
.iter()
|
||||
.map(|o| o.identifier.commit.clone())
|
||||
.collect();
|
||||
assert!(output_commits.contains(&out_commit));
|
||||
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()));
|
||||
|
|
|
@ -153,7 +153,7 @@ impl SwapServer for SwapServerImpl {
|
|||
output_commit: peeled.onion.commit,
|
||||
rangeproof: peeled.payload.rangeproof,
|
||||
input,
|
||||
fee,
|
||||
fee: fee as u64,
|
||||
onion: peeled.onion,
|
||||
status: SwapStatus::Unprocessed,
|
||||
},
|
||||
|
@ -355,6 +355,7 @@ mod tests {
|
|||
use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting};
|
||||
use secp256k1zkp::key::ZERO_KEY;
|
||||
use std::sync::Arc;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
|
||||
macro_rules! assert_error_type {
|
||||
($result:expr, $error_type:pat) => {
|
||||
|
@ -385,7 +386,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -394,7 +395,7 @@ mod tests {
|
|||
let (output_commit, proof) = secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]);
|
||||
let hop = test_util::new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||
let onion = test_util::create_onion(&input_commit, &vec![hop.clone()])?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||
|
||||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
|
@ -407,9 +408,9 @@ mod tests {
|
|||
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![],
|
||||
},
|
||||
|
@ -462,13 +463,13 @@ mod tests {
|
|||
let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
|
||||
|
||||
// Swapper data
|
||||
let swap_fee: u64 = 50_000_000;
|
||||
let swap_fee: u32 = 50_000_000;
|
||||
let (swap_sk, _swap_pk) = dalek::test_util::rand_keypair();
|
||||
let swap_hop_excess = secp::random_secret();
|
||||
let swap_hop = test_util::new_hop(&swap_sk, &swap_hop_excess, swap_fee, None);
|
||||
|
||||
// Mixer data
|
||||
let mixer_fee: u64 = 30_000_000;
|
||||
let mixer_fee: u32 = 30_000_000;
|
||||
let (mixer_sk, mixer_pk) = dalek::test_util::rand_keypair();
|
||||
let mixer_hop_excess = secp::random_secret();
|
||||
let (output_commit, proof) = secp::test_util::proof(
|
||||
|
@ -493,7 +494,7 @@ mod tests {
|
|||
output_commit.clone(),
|
||||
proof.clone(),
|
||||
)],
|
||||
kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee)?],
|
||||
kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee as u64)?],
|
||||
};
|
||||
mock_mixer.set_response(
|
||||
&vec![mixer_onion.clone()],
|
||||
|
@ -532,7 +533,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -567,7 +568,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -603,7 +604,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -638,7 +639,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -670,7 +671,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -709,7 +710,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -745,7 +746,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -777,7 +778,7 @@ mod tests {
|
|||
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)?;
|
||||
|
||||
|
@ -796,7 +797,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
Err(SwapError::FeeTooLow {
|
||||
minimum_fee: 12_500_000,
|
||||
actual_fee: fee
|
||||
actual_fee: fee as u64
|
||||
}),
|
||||
result
|
||||
);
|
||||
|
|
|
@ -221,7 +221,6 @@ mod tests {
|
|||
"{{\"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);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey};
|
||||
use crate::onion::Onion;
|
||||
use crate::types::{read_optional, write_optional};
|
||||
use crate::util::{read_optional, write_optional};
|
||||
use grin_core::core::hash::Hash;
|
||||
|
||||
use grin_core::core::Input;
|
||||
|
|
82
src/types.rs
82
src/types.rs
|
@ -1,82 +0,0 @@
|
|||
use crate::crypto::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(())
|
||||
}
|
||||
}
|
164
src/util.rs
Normal file
164
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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue