first use of sqlx

This commit is contained in:
waalge 2025-01-03 12:38:55 +00:00
commit 9c8fcfd768
11 changed files with 5102 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
db/
data/
tmp/
result
secrets/
.direnv/
target/
# Added by cargo
/target

1
.pre-commit-config.yaml Symbolic link
View File

@ -0,0 +1 @@
/nix/store/jg6yhrj8z8jb1x00fwhidh9kddzvwxj2-pre-commit-config.json

4399
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "cll2v0"
version = "0.0.0"
edition = "2021"
publish = false
# license = "MIT" -- set to apache
[package.metadata.release]
release = false
[dependencies]
anyhow = "1.0.95"
clap = { version = "4.5.18", features = ["derive"] }
ed25519-dalek = "2.1.1"
futures = "0.3.30"
hex = "0.4.3"
libp2p = { version = "0.54.1", features = ["tokio", "gossipsub", "mdns", "noise", "macros", "tcp", "yamux", "quic", "identify", "ping", "relay", "dcutr", "rendezvous", "kad"] }
libp2p-identity = { version = "0.2.9", features = ["ed25519", "peerid"] }
owo-colors = "4.1.0"
quick-protobuf = "0.8.1"
rand = "0.8.5"
serde = { version = "1.0.213", features = ["derive"] }
sqlx = { version = "0.8.2", features = ["sqlite", "macros", "runtime-tokio"] }
tokio = { version = "1.40.0", features = ["full", "tracing"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

140
README.md Normal file
View File

@ -0,0 +1,140 @@
# CL L2 V0
This repo is a sandbox for the Cardano Lightning's L2 (V0).
A key aim of this repo is to try and assess available libraries in order to
establish our key dependencies.
## Setup
This repo use nix flakes with a shell available. Otherwise ymmv.
## Context
CL depends on the use on signing/verification key pair cryptography. The signing
key may be used to handover all the participants funds. The signing key must be
handled with care.
A router node must run their infrastructure on highly available machines, in
order to provide good service to their partners. The router might choose to use
an infrastructure provider to host their nodes. The router must produce the
signatures for cheques and snapshots in the course of standard channel
operations. A priori the machine will contain signing keys. What if the machine
is compromised?
An attacker with access to the machine could send all funds to the partner of
the channel, or produce a tx that a partner signs, making it a valid mutual tx.
In either case, they are at the detriment of the router.
Instead, we may outsource the signing process to a separate machine - the
signing server. This sever can:
- serve requests to only whitelisted requesters. It can be more easily protected
from ddos type attacks and probing by an attacker.
- has minimal, relatively fixed, API. It will receive fewer updates each of
which can be reviewed more thoroughly, and have shorter/tighter software
supply chains, than might be true for the general service on the HA machine.
In addition, the signing service could be split using, say, FROST.
## Design
### Data
#### Persistent keys
These are the signing keys available to the machine and found at startup of the
service.
#### Ephemeral keys
Perhaps also called proxy keys.
An ephemeral key consists of the following properties:
1. Verification key
1. Persistent key
1. Expires at
1. Signature
### Actions
#### Sign
A sign action is the "standard" action that will occur.
A `sign` request has the following fields
1. Verification key
2. Payload
3. Signature
The request is deemed valid
1. The verification key exists in the database
1. The verification key has not expired
1. The signature is valid wrt the payload and verification key
1. The payload is "sensible" (TBC) - this is context specific.
The response to a valid request
1. The signature produced by the persistent key associated to the verification
key for the same payload.
#### Add
An add is performed by "admin" to create new ephemeral keys.
An `add` request has the following fields
1. Verification key
2. Persistent key
3. Expires at
4. Signature
The request is deemed valid
1. The persistent key is in the database
1. The signature is valid wrt the persistent key, and the payload created by
`(verification_key, expires_at)` (with some prefix?)
The result of a valid `add` request is the ephemeral key is added to the
database with the obvious fields.
The response is either `Ok()` or `Error("help message!")`
#### Revoke
A revoke is the opposite of add. It is also performed exclusively by admin.
An `revoke` request has the following fields
2. Verification key
3. Signature
The request is deemed valid
1. The verification key is in the database
1. The signature is valid wrt the associated persistent key, and the payload
created by `verification_key` (with some prefix?)
The result of a valid `revoke` request is the ephemeral key is removed from the
database.
The response is either `Ok()` or `Error("help message!")`
## Notes
### sqlx
The sqlx tool has a cli (available via the flake). This can handle migrations
provided its opinionated design choices are adopted. See
[here](https://docs.rs/sqlx/latest/sqlx/migrate/trait.MigrationSource.html).
```sh
export DATABASE_URL=sqlite:./db/cll2v0.db
sqlx database create
sqlx migration run
```
The database must be initialised before starting the application.

191
flake.lock Normal file
View File

@ -0,0 +1,191 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks-nix": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1734797603,
"narHash": "sha256-ulZN7ps8nBV31SE+dwkDvKIzvN6hroRY8sYOT0w+E28=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "f0f0dc4920a903c3e08f5bdb9246bb572fcae498",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1735264675,
"narHash": "sha256-MgdXpeX2GuJbtlBrH9EdsUeWl/yXEubyvxM1G+yO4Ak=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d49da4c08359e3c39c4e27c74ac7ac9b70085966",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1733096140,
"narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1730741070,
"narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d063c1dd113c91ab27959ba540c0d9753409edf3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"treefmt-nix": "treefmt-nix"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1735352767,
"narHash": "sha256-3zXufMRWUdwmp8/BTmxVW/k4MyqsPjLnnt/IlQyZvhc=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a16b9a7cac7f4d39a84234d62e91890370c57d76",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1735135567,
"narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "9e09d30a644c57257715902efbb3adc56c79cf28",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

62
flake.nix Normal file
View File

@ -0,0 +1,62 @@
{
description = "CL L2 V0";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-parts.url = "github:hercules-ci/flake-parts";
git-hooks-nix.url = "github:cachix/git-hooks.nix";
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake {inherit inputs;} {
imports = [
./nix/rust.nix
inputs.git-hooks-nix.flakeModule
inputs.treefmt-nix.flakeModule
];
flake = {
# Put your original flake attributes here.
};
systems = [
# systems for which you want to build the `perSystem` attributes
"x86_64-linux"
# ...
];
perSystem = {
self',
config,
inputs',
pkgs,
lib,
system,
...
}: {
treefmt = {
projectRootFile = "flake.nix";
flakeFormatter = true;
programs = {
prettier = {
enable = true;
settings = {
printWidth = 80;
proseWrap = "always";
};
};
alejandra.enable = true;
rustfmt.enable = true;
};
};
pre-commit.settings.hooks.treefmt.enable = true;
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [
inputs.rust-overlay.overlays.default
];
config = {};
};
};
};
}

13
migrations/001_keys.sql Normal file
View File

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS persistent_keys
(
id BLOB PRIMARY KEY NOT NULL
);
CREATE TABLE IF NOT EXISTS ephemeral_keys
(
id BLOB PRIMARY KEY NOT NULL,
persistent_key BLOB NOT NULL,
expires_at INTEGER NOT NULL,
signature BLOB NOT NULL,
FOREIGN KEY(persistent_key) REFERENCES persistent_keys(id)
);

92
nix/rust.nix Normal file
View File

@ -0,0 +1,92 @@
{
perSystem = {
config,
self',
inputs',
pkgs,
lib,
system,
...
}: let
osxDependencies = with pkgs;
lib.optionals stdenv.isDarwin
[
darwin.apple_sdk.frameworks.Security
darwin.apple_sdk.frameworks.CoreServices
];
cargoTomlContents = builtins.readFile ../Cargo.toml;
version = (builtins.fromTOML cargoTomlContents).package.version;
echo-net = pkgs.rustPlatform.buildRustPackage {
inherit version;
name = "cll2v0";
buildInputs = with pkgs; [openssl] ++ osxDependencies;
nativeBuildInputs = with pkgs; [pkg-config openssl.dev];
src = let
baseDir = ./../.;
fileHasAnySuffix = fileSuffixes: file: (lib.lists.any (s: lib.hasSuffix s file.name) fileSuffixes);
rustFilter = basePath: (
let
mainFilter = lib.fileset.fileFilter (fileHasAnySuffix [".rs" ".toml"]) basePath;
in
lib.fileset.unions [mainFilter (basePath + "/Cargo.toml") (basePath + "/Cargo.lock")]
);
in
pkgs.lib.fileset.toSource {
root = baseDir;
fileset = rustFilter baseDir;
};
cargoLock.lockFile = ../Cargo.lock;
meta = {
description = "Cardano Lightning L2 V0";
homepage = "cardano-lightning.org";
# license = licenses.mit;
};
};
packages = {
echo-net = echo-net;
default = packages.echo-net;
};
# FIXME :: I don't know if this is necessary,
# but I don't know to fix it.
# overlays.default = final: prev: {echo-net = packages.echo-net;};
gitRev =
if (builtins.hasAttr "rev" self')
then self'.rev
else "dirty";
in {
inherit packages; # overlays;
devShells.default = pkgs.mkShell {
nativeBuildInputs = [
config.treefmt.build.wrapper
];
shellHook = ''
export GIT_REVISION=${gitRev}
${config.pre-commit.installationScript}
'';
buildInputs = with pkgs;
[
pkg-config
openssl
cargo-insta
(pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src" "clippy" "rustfmt" "rust-analyzer"];
# targets = [ "wasm32-unknown-unknown" ];
})
sqlx-cli
sqlite
# nodePackages_latest.nodejs
# nodePackages_latest.typescript-language-server
# cmake
# wasm-pack
# protobuf
# sqlite
]
++ osxDependencies;
};
};
}

162
src/main.rs Normal file
View File

@ -0,0 +1,162 @@
use std::collections::HashMap;
use std::env;
use std::fs::read_to_string;
use clap::{Parser, Subcommand};
use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
use hex;
use sqlx::sqlite::SqlitePool;
#[derive(Parser)]
struct Args {
#[command(subcommand)]
cmd: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Add {
ephemeral_key: String,
persistent_key: String,
expires_at: i64,
signature: String,
},
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let keys_path = env::var("CLL2V0_KEYS").expect("Expect `CLL2V0_KEYS` to be set");
// let db_url = env::var("CLL2V0_DB").expect("Expect `CLL2V0_DB` to be set");
let db_url = env::var("DATABASE_URL").expect("Expect `DATABASE_URL` to be set");
let signing_keys = read_to_string(&keys_path).expect("Error reading file");
let pool = SqlitePool::connect(&db_url).await?;
let mut keychain: HashMap<[u8; 32], SigningKey> = HashMap::new();
for key in signing_keys.lines() {
let secret_key = hex::decode(key.trim()).unwrap().try_into().unwrap();
let skey = SigningKey::from_bytes(&secret_key);
let vkey = skey.verifying_key();
keychain.insert(vkey.to_bytes(), skey);
let _ = add_persistent_key(&pool, vkey).await;
}
match args.cmd {
Some(Command::Add {
ephemeral_key,
persistent_key,
expires_at,
signature,
}) => {
println!("Adding new ephemeral key '{ephemeral_key}'");
let e = VerifyingKey::from_bytes(
&hex::decode(ephemeral_key)
.expect("Cannot parse ephemeral key")
.try_into()
.expect("wrong length"),
)
.expect("bad key");
let p = VerifyingKey::from_bytes(
&hex::decode(persistent_key)
.expect("Cannot parse persistent")
.try_into()
.expect("wrong length"),
)
.expect("bad key");
let s = Signature::from_bytes(
&hex::decode(signature)
.expect("Cannot parse persistent")
.try_into()
.expect("wrong length"),
);
let res = add_ephemeral_key(&pool, &e, &p, expires_at, &s).await?;
println!("Added new ekey: {}", res);
}
None => {
println!("Printing list of all todos");
list_persistent_keys(&pool).await?;
}
}
Ok(())
}
async fn add_persistent_key(pool: &SqlitePool, vkey: VerifyingKey) -> anyhow::Result<i64> {
let mut conn = pool.acquire().await?;
let b = Into::<Vec<u8>>::into(vkey.to_bytes());
// Insert the task, then obtain the ID of this row
let id = sqlx::query!(
r#"
INSERT INTO persistent_keys ( id )
VALUES ( ?1 )
"#,
b
)
.execute(&mut *conn)
.await?
.last_insert_rowid();
Ok(id)
}
async fn add_ephemeral_key(
pool: &SqlitePool,
ephemeral_key: &VerifyingKey,
persistent_key: &VerifyingKey,
expires_at: i64,
signature: &Signature,
) -> anyhow::Result<i64> {
let mut conn = pool.acquire().await?;
let ephemeral_key_ = Into::<Vec<u8>>::into(ephemeral_key.to_bytes());
let persistent_key_ = Into::<Vec<u8>>::into(persistent_key.to_bytes());
let signature_ = Into::<Vec<u8>>::into(signature.to_bytes());
// Insert the task, then obtain the ID of this row
let res = sqlx::query!(
r#"
INSERT INTO ephemeral_keys (
id,
persistent_key,
expires_at,
signature
)
VALUES ( ?1, ?2, ?3, ?4 )
"#,
ephemeral_key_,
persistent_key_,
expires_at,
signature_,
)
.execute(&mut *conn)
.await?
.last_insert_rowid();
Ok(res)
}
async fn list_persistent_keys(pool: &SqlitePool) -> anyhow::Result<()> {
let recs = sqlx::query!(
r#"
SELECT id
FROM persistent_keys
ORDER BY id
"#
)
.fetch_all(pool)
.await?;
for rec in recs {
println!("- [{}]", hex::encode(rec.id),);
}
Ok(())
}
fn print_key() {
// Useful for setting up some keys
let mut rng = rand::rngs::OsRng;
println!("{}", hex::encode(SigningKey::generate(&mut rng).to_bytes()));
}