Skip to content

Commit

Permalink
make newkeys method an RPC call
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamISZ committed Jul 17, 2024
1 parent 412d3eb commit 4264440
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 54 deletions.
62 changes: 37 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,33 @@ sudo apt install build-essential

Build the project with `cargo build --release` (without release flag, the debug version is very slow), then the executable is at `target/release/autct`.

Start with `target/release/autct --help` for the summary of the syntax. Note that two flags are required (TODO: they should be arguments), namely `-M` for the mode and `-k` for the keyset.
Start with `target/release/autct --help` for the summary of the syntax. Note that two flags are required (TODO: they should be arguments), namely `-M` for the mode/method and `-k` for the keyset.

Taking each of the four `mode`s in turn:

"serve":

```
target/release/autct -M serve --keysets \
my-context:testdata/autct-203015-500000-0-2-1024.aks,my-other-context:some-other-filepath -n signet
```

The application's architecture is based around the idea of a (potentially always-on) daemon acting as an RPC server, for clients to request actions from. This allows easier usage by external applications written in different environments/languages etc., and means that such clients do **not** need to have implemented any of the custom cryptographic operations.

Currently the application supports three specific distinct RPC requests, represented by the modes `prove`, `verify` and `newkeys`.

The RPC server will often takes some considerable time to start up (1-2 minutes e.g.) (loading precomputation tables and constructing the Curve Tree), and then serves on port as specified with `-p` at host specified with `-H` (default 127.0.0.1:23333).

Additionally, a server can serve the proving and verification function for multiple different contexts simultaneously, by extending the comma-separated list as shown above. Each item in the list must have a different context label, and the keyset files for each can be the same or different, as desired.

"newkeys":

```
./autct -M newkeys --keysets none -n mainnet -i privkey-file
```

If you need a new taproot address and its corresponding private key, this convenience method allows that. The private key is written in WIF format to the file specified with the `-i` flag, and can be imported into other taproot-supporting wallets; the address is the 'standard' p2tr type. The network (`-n`) should be one of `mainnet`, `signet` or `regtest`.

"prove":

```
Expand All @@ -83,24 +106,15 @@ my-context:testdata/autct-203015-500000-0-2-1024.aks \
-i privkeyfile
```

Note that the private key is read in as WIF format from the file specified with `-i`, which by default is a local directory file called `privkey`.
As per `newkeys` above, the private key is read in as WIF format from the file specified with `-i` (the default is a local directory file called `privkey`).

Note that the `keysets` (or `-k`) option takes a particular format: `contextlabel:filename`, and that this can (as we will see below) be a comma-separated list for other operations, but for proving just use one. The idea here is that usage tokens' scarcity depends on context; you can use the same utxo twice in *different* contexts (like, different applications) but only once in the same context.
Note that the `keysets` (or `-k`) option takes a particular format: `contextlabel:filename`, and that this can be a comma-separated list for the `serve` operation, but for proving and veryifying, you must just use one. The idea of `context-label` is that usage tokens' scarcity depends on context; you can use the same utxo twice in *different* contexts (like, different applications) but only once in the same context.

The file `autct-203015-500000-0-2-1024.aks`, or whatever else is specified (see [here](./docs/protocol-utxo.md) Appendix 1 for filename structure), should contain public keys in format: compressed, hex encoded, separated by whitespace, all on one line.

The output of the proving algorithm is sent to the file specified by `-P`, which should usually be around 2-3kB. The program will look for the pubkey corresponding to the given private key, in the list of pubkeys in the pubkey file, in order to identify the correct index to use in the proof.

"serve":

```
target/release/autct -M serve --keysets \
my-context:testdata/autct-203015-500000-0-2-1024.aks,my-other-context:some-other-filepath
```

As probably obvious, the idea here is that we run an RPC server (somewhere) for a client to be able to make proof requests, or give serialized proofs to to make verification requests, i.e. if the proof and the corresponding key image actually validate against the curve tree specified. If so, the user can credit whoever provided this proof, with some kind of token, service access, whatever, and also the code keep track of what key images have already been used in a flat file persistent storage (see config option `keyimage_filename_suffix`). Here "quickly" should be in the 50-100ms range, for even up to millions of pubkeys. The RPC server will often takes some considerable time to start up (1-2 minutes e.g.) (loading precomputation tables and constructing the Curve Tree), and then serves on port as specified with `-p` at host specified with `-H` (default 127.0.0.1:23333).

Additionally, as noted above, a server can serve the proving and verification function for multiple different contexts simultaneously, by extending the comma-separated list as shown above. Each item in the list must have a different context label, and the keyset files for each can be the same or different, as desired.
Note that in contrast to verification as specified below, proving can take a non trivial time (15 seconds for large keysets is not untypical).

"verify":

Expand All @@ -113,16 +127,6 @@ This client connects to the above server and calls the `verify()` function with

In the directory `testdata` there is an example pubkey file containing approximately 330K pubkeys taken from all taproot utxos on signet at block 203015, which you can use to test if you like. For this pubkey set, the private key `cRczLRUHjDTEM92wagj3mMRvP69Jz3SEHQc8pFKiszFBPpJo8dVD` is for one of those pubkeys (signet!), so if you use it, the proof should verify, and the key image you get as output from the verifier should be: `2e7b894455872f3039fb734b42534be410a2a2237a08212b4c9a5bd039c6b4d080` (with the default test labels as per the worked example below).

Finally, an auxiliary tool:

"newkeys":

```
./autct -M newkeys --keysets none -n mainnet
```

If you need a new taproot address and its corresponding private key, this convenience method allows that. The private key is output in WIF format and can be imported into other taproot-supporting wallets; the address is the 'standard' p2tr type. The network (`-n`) should be one of `mainnet`, `signet` or `regtest`.

## Configuring

Use `target/release/autct --help` for documentation of the options available; these are the same settings you can set in the config file:
Expand All @@ -131,7 +135,7 @@ The config file is auto-generated in `~/.config/autct/default-config.toml` (or s

Precedence operation is as you would expect: command line options take precedence over config file values, and be aware updates (i.e. just choosing a different option in a command line call) will be persisted to that config file. Third in order of precedence is the default value. As noted, two "options" (`-M` and `-k`) are required to be specified always.

The depth and branching factor are the parameters of the curve tree. The `generators_length_log_2` may be removed in future but it should be the smallest power of 2 that's bigger than `D(912+L-1)` where `D` is the depth and `L` is the branching factor. If it helps, for key sets up to 500K in size, the defaults should be fine. The rpc port can also be configured here.
The depth and branching factor are the parameters of the curve tree. The `generators_length_log_2` may be removed in future but it should be the smallest power of 2 that's bigger than `D(912+L-1)` where `D` is the depth and `L` is the branching factor. If it helps, for keysets up to 500K in size, the defaults should be fine. The rpc port can also be configured here.

Finally, one *may* need to set the `user_string` with `-u` to a hex serialized BIP340 pubkey or alternate user string (see [here](./docs/protocol-utxo.md) Appendix 2). This defines "who" can use the resources accessed by "consuming" the utxo in that context.

Expand Down Expand Up @@ -172,7 +176,7 @@ If you check the other terminal you will see some debug output as the proof was
Next make a request to verify the proof and deliver a resource:

```
target/release/autct -M request -P default-proof-file -k \
target/release/autct -M verify -P default-proof-file -k \
my-context:testdata/autct-203015-500000-0-2-1024.aks
```

Expand All @@ -197,6 +201,14 @@ Configuration file: '/home/user/.config/autct/default-config.toml'
Request was accepted by the Autct verifier! The proof is valid and the (unknown) pubkey is unused.
```

Note that if you repeat this test, you will get instead:

```
Request rejected, proofs are valid but key image is reused.
```

which is correct; key image was stored in the file `autct-v1.0my-contextkeyimages.aki` and reuse will not be allowed unless that file is deleted.

(Process currently verified working on Ubuntu 22.04, Debian 12 and Windows 10)

# Testing
Expand Down
46 changes: 20 additions & 26 deletions src/autct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ extern crate ark_secp256k1;
use autct::rpcclient;
use autct::rpcserver;
use autct::config::AutctConfig;
use bitcoin::{Address, PrivateKey, XOnlyPublicKey};
use bitcoin::key::Secp256k1;


use std::error::Error;
Expand All @@ -22,38 +20,34 @@ async fn main() -> Result<(), Box<dyn Error>>{
"verify" => {return request_verify(autctcfg).await},
"serve" => {return rpcserver::do_serve(autctcfg).await
},
"newkeys" => {return create_keys(autctcfg).await},
"newkeys" => {return request_create_keys(autctcfg).await},
_ => {return Err("Invalid mode, must be 'prove', 'serve', 'newkeys' or 'verify'".into())},

}
}

async fn create_keys(autctcfg: AutctConfig) ->Result<(), Box<dyn Error>> {

let nw = match autctcfg.bc_network.unwrap().as_str() {
"mainnet" => bitcoin::Network::Bitcoin,
"signet" => bitcoin::Network::Signet,
"regtest" => bitcoin::Network::Regtest,
_ => return Err("Invalid bitcoin network string in config.".into()),
async fn request_create_keys(autctcfg: AutctConfig) ->Result<(), Box<dyn Error>> {
let res = rpcclient::createkeys(autctcfg).await;
match res {
Ok(rest) => {
// codes defined in lib.rs
match rest.accepted {
0 => {println!("New key and address generated successfully");
println!("This is the address to pay into: {}", rest.address.unwrap());
println!("The corresponding private key is written in WIF format to: {}", rest.privkey_file_loc.unwrap());
println!("The WIF string can be imported into e.g. Sparrow, Core to sweep or access the funds in it.");
},
-1 => println!("Undefined failure in key generation."),
-2 => println!("New key request rejected, mismatch in bitcoin network."),
-3 => println!("New key request rejected, could not write private key to specified file location."),
_ => println!("Unrecognized error code from server?"),
}
},
Err(_) => return Err("Proving request processing failed.".into()),
};

// This uses the `rand-std` feature in the rust-bitcoin crate to generate
// the random number via libsecp256k1 in the recommended secure way:
let privkey = PrivateKey::generate(nw);
let secp = Secp256k1::new();
// this is the standard way to generate plain-vanilla taproot addresses:
// it is not "raw" (rawtr in descriptors) but it applies a merkle root of
// null as the tweak to the internal pubkey. This is what e.g. Sparrow is
// looking for as "p2tr" type.
let addr = Address::p2tr(&secp,
XOnlyPublicKey::from(privkey.public_key(&secp).inner),
None, privkey.network);
println!("This is the address to pay into: {}", addr);
// print this to the file configured in --privkey file, but error/warn if already exists
println!("This is the private key in WIF format: {}", privkey);
println!("The WIF string above can be imported into e.g. Sparrow, Core to sweep or access the funds in it.");
Ok(())
}

async fn request_verify(autctcfg: AutctConfig) -> Result<(), Box<dyn Error>> {
let res = rpcclient::verify(autctcfg).await;
match res {
Expand Down
63 changes: 61 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ use ark_secq256k1::Config as SecqConfig;
use std::io::Cursor;
use std::time::Instant;
use toy_rpc::macros::export_impl;
use std::io::Write;

//use autct::get_curve_tree_with_proof;
use base64::prelude::*;

use bitcoin::key::{Secp256k1, TapTweak, UntweakedKeypair};
Expand All @@ -38,7 +38,7 @@ use alloc::vec::Vec;
use ark_ec::{AffineRepr, short_weierstrass::SWCurveConfig, CurveGroup};
use ark_secp256k1::Fq as SecpBase;
use std::ops::{Mul, Add};
use bitcoin::PrivateKey;
use bitcoin::{Address, PrivateKey, XOnlyPublicKey};

pub mod rpc {

Expand All @@ -60,6 +60,65 @@ pub mod rpc {
pub ks: Vec<Arc<Mutex<keyimagestore::KeyImageStore<Affine<SecpConfig>>>>>,
}

#[derive(Serialize, Deserialize)]
pub struct RPCCreateKeysRequest {
pub bc_network: String,
pub privkey_file_loc: String
}

#[derive(Serialize, Deserialize)]
pub struct RPCCreateKeysResponse {
pub address: Option<String>, // note that for taproot, the pubkey is implicit
pub privkey_file_loc: Option<String>,
pub accepted: i32,
}

pub struct RPCCreateKeys {
}

#[export_impl]
impl RPCCreateKeys {
#[export_method]
pub async fn createkeys(&self, args: RPCCreateKeysRequest)
-> Result<RPCCreateKeysResponse, String> {
let mut resp: RPCCreateKeysResponse = RPCCreateKeysResponse{
address: None,
privkey_file_loc: Some(args.privkey_file_loc),
accepted: -1
};
let nw = match args.bc_network.as_str() {
"mainnet" => bitcoin::Network::Bitcoin,
"signet" => bitcoin::Network::Signet,
"regtest" => bitcoin::Network::Regtest,
_ => {resp.accepted = -2;
return Ok(resp)},
};

// This uses the `rand-std` feature in the rust-bitcoin crate to generate
// the random number via libsecp256k1 in the recommended secure way:
let privkey = PrivateKey::generate(nw);
// persist the newly created key in WIF format to the location
// requested:
let mut buf = Vec::new();
let res = write!(&mut buf, "{}", privkey);
if !res.is_ok(){
resp.accepted = -3;
return Ok(resp);
}
write_file_string(&resp.privkey_file_loc.clone().unwrap(), buf);
let secp = Secp256k1::new();
// this is the standard way to generate plain-vanilla taproot addresses:
// it is not "raw" (rawtr in descriptors) but it applies a merkle root of
// null as the tweak to the internal pubkey. This is what e.g. Sparrow is
// looking for as "p2tr" type.
let addr = Address::p2tr(&secp,
XOnlyPublicKey::from(privkey.public_key(&secp).inner),
None, privkey.network);
resp.address = Some(addr.to_string());
resp.accepted = 0;
Ok(resp)
}
}
#[derive(Serialize, Deserialize)]
pub struct RPCProverRequest {
pub keyset: String,
Expand Down
23 changes: 23 additions & 0 deletions src/rpcclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ pub async fn verify(autctcfg: AutctConfig) -> Result<RPCProofVerifyResponse, Box
Ok(result)
}

pub async fn createkeys(autctcfg: AutctConfig) -> Result<RPCCreateKeysResponse, Box<dyn Error>> {
let rpc_port = autctcfg.rpc_port;
let host: &str= &autctcfg.rpc_host.clone().unwrap();
let port_str: &str = &rpc_port.unwrap().to_string();
let addr: String = format!("{}:{}", host, port_str);
let req: RPCCreateKeysRequest = RPCCreateKeysRequest {
bc_network: autctcfg.bc_network.unwrap(),
privkey_file_loc: autctcfg.privkey_file_str.unwrap()
};
let mut client = Client::dial(&addr).await.unwrap();
client.set_default_timeout(std::time::Duration::from_secs(3));
let result = client
.r_p_c_create_keys().createkeys(req)
.await;
match result {
Ok(x) => return Ok(x),
Err(x) => {
println!("Error in rpc client prove call: {}", &x);
return Err(x.into());
}
}
}

pub async fn prove(autctcfg: AutctConfig) -> Result<RPCProverResponse, Box<dyn Error>>{
let rpc_port = autctcfg.rpc_port;
let host: &str= &autctcfg.rpc_host.clone().unwrap();
Expand Down
4 changes: 3 additions & 1 deletion src/rpcserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use ark_serialize::{ CanonicalDeserialize,
Compress, Validate};
use crate::rpc::RPCProverVerifierArgs;
use crate::rpc::{RPCCreateKeys, RPCProverVerifierArgs};
use crate::utils::{get_curve_tree, get_leaf_commitments, convert_keys, APP_DOMAIN_LABEL};
use tokio::{task, net::TcpListener};
use std::fs;
Expand Down Expand Up @@ -98,9 +98,11 @@ pub async fn do_serve(autctcfg: AutctConfig) -> Result<(), Box<dyn Error>>{
RPCProver{
prover_verifier_args}
);
let createkeys_service = Arc::new(RPCCreateKeys{});
let server = Server::builder()
.register(verifier_service) // register service
.register(prover_service)
.register(createkeys_service)
.build();
let listener = TcpListener::bind(&addr).await.unwrap();

Expand Down

0 comments on commit 4264440

Please sign in to comment.