Skip to content

Commit 0666ca6

Browse files
committed
Add VSS Store Implementation
1 parent 5c1f155 commit 0666ca6

File tree

6 files changed

+223
-0
lines changed

6 files changed

+223
-0
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ jobs:
4848
cargo update -p regex --precise "1.9.6" --verbose # regex 1.10.0 requires rustc 1.65.0
4949
cargo update -p jobserver --precise "0.1.26" --verbose # jobserver 0.1.27 requires rustc 1.66.0
5050
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5" --verbose # zstd-sys 2.0.9+zstd.1.5.5 requires rustc 1.64.0
51+
cargo update -p petgraph --precise "0.6.3" --verbose # petgraph v0.6.4, requires rustc 1.64 or newer
5152
- name: Build on Rust ${{ matrix.toolchain }}
5253
run: cargo build --verbose --color always
5354
- name: Build with UniFFI support on Rust ${{ matrix.toolchain }}

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ panic = 'abort' # Abort on panic
3030

3131
[features]
3232
default = []
33+
vss = []
34+
vss-test = []
3335

3436
[dependencies]
3537
lightning = { version = "0.0.118", features = ["max_level_trace", "std"] }
@@ -70,6 +72,7 @@ tokio = { version = "1", default-features = false, features = [ "rt-multi-thread
7072
esplora-client = { version = "0.4", default-features = false }
7173
libc = "0.2"
7274
uniffi = { version = "0.23.0", features = ["build"], optional = true }
75+
vss-client = "0.1"
7376

7477
[target.'cfg(windows)'.dependencies]
7578
winapi = { version = "0.3", features = ["winbase"] }

src/builder.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ use bip39::Mnemonic;
4848

4949
use bitcoin::BlockHash;
5050

51+
#[cfg(feature = "vss")]
52+
use crate::io::vss_store::VssStore;
5153
use std::convert::TryInto;
5254
use std::default::Default;
5355
use std::fmt;
@@ -269,6 +271,16 @@ impl NodeBuilder {
269271
self.build_with_store(kv_store)
270272
}
271273

274+
/// Builds a [`Node`] instance with a [`VssStore`] backend and according to the options
275+
/// previously configured.
276+
#[cfg(feature = "vss")]
277+
pub fn build_with_vss_store(
278+
&self, url: &str, store_id: String,
279+
) -> Result<Node<VssStore>, BuildError> {
280+
let vss = Arc::new(VssStore::new(url, store_id));
281+
self.build_with_store(vss)
282+
}
283+
272284
/// Builds a [`Node`] instance according to the options previously configured.
273285
pub fn build_with_store<K: KVStore + Sync + Send + 'static>(
274286
&self, kv_store: Arc<K>,

src/io/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod sqlite_store;
44
#[cfg(test)]
55
pub(crate) mod test_utils;
66
pub(crate) mod utils;
7+
pub(crate) mod vss_store;
78

89
/// The event queue will be persisted under this key.
910
pub(crate) const EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE: &str = "";

src/io/vss_store.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use io::Error;
2+
use std::io;
3+
use std::io::ErrorKind;
4+
#[cfg(test)]
5+
use std::panic::RefUnwindSafe;
6+
7+
use crate::io::utils::check_namespace_key_validity;
8+
use lightning::util::persist::KVStore;
9+
use tokio::runtime::Runtime;
10+
use vss_client::client::VssClient;
11+
use vss_client::error::VssError;
12+
use vss_client::types::{
13+
DeleteObjectRequest, GetObjectRequest, KeyValue, ListKeyVersionsRequest, PutObjectRequest,
14+
};
15+
16+
/// A [`KVStore`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend.
17+
pub struct VssStore {
18+
client: VssClient,
19+
store_id: String,
20+
runtime: Runtime,
21+
}
22+
23+
impl VssStore {
24+
#[cfg(feature = "vss")]
25+
pub(crate) fn new(base_url: &str, store_id: String) -> Self {
26+
let client = VssClient::new(base_url);
27+
let runtime = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
28+
Self { client, store_id, runtime }
29+
}
30+
31+
fn build_key(&self, namespace: &str, sub_namespace: &str, key: &str) -> io::Result<String> {
32+
if key.is_empty() {
33+
return Err(Error::new(ErrorKind::Other, "Empty key is not allowed"));
34+
}
35+
// But namespace and sub_namespace can be empty
36+
if namespace.is_empty() {
37+
Ok(key.to_string())
38+
} else {
39+
Ok(format!("{}#{}#{}", namespace, sub_namespace, key))
40+
}
41+
}
42+
43+
fn split_key(&self, key: &str) -> io::Result<(String, String, String)> {
44+
let parts: Vec<&str> = key.split('#').collect();
45+
match parts.as_slice() {
46+
[namespace, sub_namespace, actual_key] => {
47+
Ok((namespace.to_string(), sub_namespace.to_string(), actual_key.to_string()))
48+
}
49+
_ => Err(Error::new(ErrorKind::InvalidData, "Invalid key format")),
50+
}
51+
}
52+
53+
async fn list_all_keys(&self, namespace: &str, sub_namespace: &str) -> io::Result<Vec<String>> {
54+
let mut page_token = None;
55+
let mut keys = vec![];
56+
let key_prefix = format!("{}#{}", namespace, sub_namespace);
57+
while page_token != Some("".to_string()) {
58+
let request = ListKeyVersionsRequest {
59+
store_id: self.store_id.to_string(),
60+
key_prefix: Some(key_prefix.to_string()),
61+
page_token,
62+
page_size: None,
63+
};
64+
65+
let response = self.client.list_key_versions(&request).await.map_err(|e| {
66+
let msg = format!("Failed to list keys in {}/{}: {}", namespace, sub_namespace, e);
67+
Error::new(ErrorKind::Other, msg)
68+
})?;
69+
70+
for kv in response.key_versions {
71+
keys.push(self.split_key(&kv.key)?.2);
72+
}
73+
page_token = response.next_page_token;
74+
}
75+
Ok(keys)
76+
}
77+
}
78+
79+
impl KVStore for VssStore {
80+
fn read(&self, namespace: &str, sub_namespace: &str, key: &str) -> io::Result<Vec<u8>> {
81+
check_namespace_key_validity(namespace, sub_namespace, Some(key), "read")?;
82+
let request = GetObjectRequest {
83+
store_id: self.store_id.to_string(),
84+
key: self.build_key(namespace, sub_namespace, key)?,
85+
};
86+
// self.runtime.spawn()
87+
let resp =
88+
tokio::task::block_in_place(|| self.runtime.block_on(self.client.get_object(&request)))
89+
.map_err(|e| match e {
90+
VssError::NoSuchKeyError(..) => {
91+
let msg = format!(
92+
"Failed to read as key could not be found: {}/{}. Details: {}",
93+
namespace, key, e
94+
);
95+
Error::new(ErrorKind::NotFound, msg)
96+
}
97+
_ => {
98+
let msg = format!("Failed to read from key {}/{}: {}", namespace, key, e);
99+
Error::new(ErrorKind::Other, msg)
100+
}
101+
})?;
102+
Ok(resp.value.unwrap().value)
103+
}
104+
105+
fn write(&self, namespace: &str, sub_namespace: &str, key: &str, buf: &[u8]) -> io::Result<()> {
106+
check_namespace_key_validity(namespace, sub_namespace, Some(key), "write")?;
107+
let request = PutObjectRequest {
108+
store_id: self.store_id.to_string(),
109+
global_version: None,
110+
transaction_items: vec![KeyValue {
111+
key: self.build_key(namespace, sub_namespace, key)?,
112+
version: -1,
113+
value: buf.to_vec(),
114+
}],
115+
delete_items: vec![],
116+
};
117+
118+
tokio::task::block_in_place(|| self.runtime.block_on(self.client.put_object(&request)))
119+
.map_err(|e| {
120+
let msg = format!("Failed to write to key {}/{}: {}", namespace, key, e);
121+
Error::new(ErrorKind::Other, msg)
122+
})?;
123+
124+
Ok(())
125+
}
126+
127+
fn remove(
128+
&self, namespace: &str, sub_namespace: &str, key: &str, _lazy: bool,
129+
) -> io::Result<()> {
130+
check_namespace_key_validity(namespace, sub_namespace, Some(key), "remove")?;
131+
let request = DeleteObjectRequest {
132+
store_id: self.store_id.to_string(),
133+
key_value: Some(KeyValue {
134+
key: self.build_key(namespace, sub_namespace, key)?,
135+
version: -1,
136+
value: vec![],
137+
}),
138+
};
139+
140+
tokio::task::block_in_place(|| self.runtime.block_on(self.client.delete_object(&request)))
141+
.map_err(|e| {
142+
let msg = format!("Failed to delete key {}/{}: {}", namespace, key, e);
143+
Error::new(ErrorKind::Other, msg)
144+
})?;
145+
Ok(())
146+
}
147+
148+
fn list(&self, namespace: &str, sub_namespace: &str) -> io::Result<Vec<String>> {
149+
check_namespace_key_validity(namespace, sub_namespace, None, "list")?;
150+
151+
let keys = tokio::task::block_in_place(|| {
152+
self.runtime.block_on(self.list_all_keys(namespace, sub_namespace))
153+
})
154+
.map_err(|e| {
155+
let msg = format!("Failed to retrieve keys in namespace: {} : {}", namespace, e);
156+
Error::new(ErrorKind::Other, msg)
157+
})?;
158+
159+
Ok(keys)
160+
}
161+
}
162+
163+
#[cfg(test)]
164+
impl RefUnwindSafe for VssStore {}
165+
166+
#[cfg(test)]
167+
#[cfg(feature = "vss-test")]
168+
mod tests {
169+
use super::*;
170+
use crate::io::test_utils::do_read_write_remove_list_persist;
171+
use rand::distributions::Alphanumeric;
172+
use rand::{thread_rng, Rng};
173+
174+
#[test]
175+
fn read_write_remove_list_persist() {
176+
let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap();
177+
let mut rng = thread_rng();
178+
let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect();
179+
let vss_store = VssStore::new(&vss_base_url, rand_store_id);
180+
181+
do_read_write_remove_list_persist(&vss_store);
182+
}
183+
}

src/test/functional_tests.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,29 @@ fn channel_full_cycle() {
1818
do_channel_full_cycle(node_a, node_b, &bitcoind, &electrsd, false);
1919
}
2020

21+
#[test]
22+
#[cfg(feature = "vss-test")]
23+
fn channel_full_cycle_with_vss_store() {
24+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
25+
println!("== Node A ==");
26+
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
27+
let config_a = random_config();
28+
let mut builder_a = NodeBuilder::from_config(config_a);
29+
builder_a.set_esplora_server(esplora_url.clone());
30+
let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap();
31+
let node_a = builder_a.build_with_vss_store(&vss_base_url, "node_1_store".to_string()).unwrap();
32+
node_a.start().unwrap();
33+
34+
println!("\n== Node B ==");
35+
let config_b = random_config();
36+
let mut builder_b = NodeBuilder::from_config(config_b);
37+
builder_b.set_esplora_server(esplora_url);
38+
let node_b = builder_b.build_with_vss_store(&vss_base_url, "node_2_store".to_string()).unwrap();
39+
node_b.start().unwrap();
40+
41+
do_channel_full_cycle(node_a, node_b, &bitcoind, &electrsd, false);
42+
}
43+
2144
#[test]
2245
fn channel_full_cycle_0conf() {
2346
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)