102 Commits

Author SHA1 Message Date
Dave Hrycyszyn
ba585d0888 Noted the (dis-)use of Electrum client 2024-06-26 18:12:52 +01:00
Dave Hrycyszyn
213b8f22fe Added some docs regaring the Bitcoin clients 2024-06-26 18:11:42 +01:00
Dave Hrycyszyn
446efc2fbf Noted that we're signing as well as building tx in the method signature 2024-06-25 17:32:44 +01:00
Dave Hrycyszyn
c7095ced7b ibid 2024-06-25 15:23:52 +01:00
Dave Hrycyszyn
70d1b1eed9 Extracted a persist_local() function 2024-06-25 15:23:42 +01:00
Dave Hrycyszyn
93e66ba8b5 Noted deprectaion of the glorious electrum client.
I'll keep it around for a while in case I run into trouble with esplora/mutiny
2024-06-25 15:20:13 +01:00
Dave Hrycyszyn
6b1aa2b4ca Sending between multiple addresses works nicely 2024-06-25 15:18:30 +01:00
Dave Hrycyszyn
a6105cf2bf Made the wallet's Network configurable 2024-06-25 15:05:33 +01:00
Dave Hrycyszyn
7effe9455f Some docs on the bitcoin client 2024-06-25 15:02:50 +01:00
Dave Hrycyszyn
cf116829f8 Naming driver users 2024-06-25 14:58:53 +01:00
Dave Hrycyszyn
35deb4a75c Extracted the Esplora wallet into a struct 2024-06-25 14:58:13 +01:00
Dave Hrycyszyn
2474f5186d More refactoring to make the driver do the work 2024-06-25 14:43:57 +01:00
Dave Hrycyszyn
c6242e99f7 Pulled the client apart into (somewhat) re-usable functions 2024-06-25 14:35:42 +01:00
Dave Hrycyszyn
5abc05a8a9 Starting to separate the esplora client funcionality from business logic
Having a driver will allow us to start experimenting with transaction signing
more easily
2024-06-25 14:13:06 +01:00
Dave Hrycyszyn
6b29d49aaa Fixing CRDT tests 2024-06-25 14:09:41 +01:00
Dave Hrycyszyn
9c00a7f30a Getting rid of "Debug" derived implementation in the macro 2024-06-25 14:04:43 +01:00
Dave Hrycyszyn
28ddb07126 Removing unused code 2024-06-25 13:57:52 +01:00
Dave Hrycyszyn
3cbde1262e Removed unused client directory 2024-06-25 13:54:55 +01:00
Dave Hrycyszyn
a1e62ebb51 Moved the websocket client into bft_crdt module 2024-06-25 13:54:10 +01:00
Dave Hrycyszyn
e6a4fe0fd6 Moved all the bft_crdt stuff into its own module 2024-06-25 13:50:02 +01:00
Dave Hrycyszyn
7fb4585deb Renamed bitcoin clients 2024-06-25 13:38:23 +01:00
Dave Hrycyszyn
73f33a61e6 Simplified bitcoin keys module name 2024-06-25 13:36:50 +01:00
Dave Hrycyszyn
037fc27b7b Starting a refactor into more functional modules 2024-06-25 13:31:02 +01:00
Dave Hrycyszyn
d4809a48e6 Docs on the bdk_wallet esplora client. 2024-06-25 13:08:37 +01:00
Dave Hrycyszyn
bedbd54fae Some docs as to current problems with the bare bdk wallet crate. 2024-06-25 13:07:07 +01:00
Dave Hrycyszyn
b08e69ab1b cleanup 2024-06-24 18:47:27 +01:00
Dave Hrycyszyn
0ad430a9f1 Switching to Mutiny Net for 30 second block times. 2024-06-24 18:47:03 +01:00
Dave Hrycyszyn
a516de4bcb Esplora client now working with persisted mnemonics 2024-06-24 18:41:55 +01:00
Dave Hrycyszyn
117915bded Splitting key load / wallet creation so we can use keys in esplora client 2024-06-24 17:20:52 +01:00
Dave Hrycyszyn
5c03a77e56 Bit of cleanup after all the excitement 2024-06-24 16:43:17 +01:00
Dave Hrycyszyn
d59fa78cd7 What an odyssey! Bitcoin sends now work. 2024-06-24 16:31:03 +01:00
Dave Hrycyszyn
9e4d9a4762 Switching to Bitcoin Testnet as Signet does not appear to work with Electrum atm 2024-06-24 15:57:31 +01:00
Dave Hrycyszyn
462590b82f Simplified bdk client 2024-06-24 13:56:57 +01:00
Dave Hrycyszyn
9e19500ab0 Comment on wallet loader 2024-06-24 13:56:43 +01:00
Dave Hrycyszyn
643a0d7f52 WIP 2024-06-24 08:02:17 +01:00
Dave Hrycyszyn
d6c118ca3b Generating a new wallet with mnemonic works nicely 2024-06-21 18:18:52 +01:00
Dave Hrycyszyn
d0f75d443b Getting ready for mnemonic/key generation 2024-06-21 17:45:37 +01:00
Dave Hrycyszyn
ac6473bb1b Ok the bdk looks like a far better bet! 2024-06-21 17:00:01 +01:00
Dave Hrycyszyn
933fea76df Going to try out the bdk 2024-06-21 16:34:53 +01:00
Dave Hrycyszyn
14f24c6d34 WIP commit with rustbitcoin-rpc, which is deeply unpleasant and unfinished 2024-06-21 16:26:43 +01:00
Dave Hrycyszyn
c5a6aeb067 Added a working btc-rpc client, works with a running local signet node 2024-06-20 19:46:56 +01:00
Dave Hrycyszyn
13e144f19e Implemented a blank Btc command 2024-06-20 17:21:41 +01:00
Dave Hrycyszyn
c0c5a12e84 Added a bitcoin client 2024-06-20 17:13:56 +01:00
Dave Hrycyszyn
53b17591b8 Fixed bitcoin tx compilation (currently unused) 2024-06-20 17:13:47 +01:00
Dave Hrycyszyn
a29a0fca04 Moved keys submodules 2024-06-20 17:13:34 +01:00
Dave Hrycyszyn
1ad7c99283 wip btc 2024-06-18 17:43:32 +01:00
Dave Hrycyszyn
60e87383b0 Getting ready to format a Bitcoin transaction 2024-06-18 17:12:05 +01:00
Dave Hrycyszyn
089201b7be Removed unused import 2024-06-18 17:04:29 +01:00
Dave Hrycyszyn
8e7d24ec7b Bitcoin keys now load into SideNode 2024-06-18 17:03:31 +01:00
Dave Hrycyszyn
706a671902 wip adding bitcoin keys to side nodes 2024-06-18 16:56:24 +01:00
Dave Hrycyszyn
ecec883f9b Renamed keys module 2024-06-18 16:35:56 +01:00
Dave Hrycyszyn
ae8a70e249 Renaming keys to bft_crdt_keys 2024-06-18 16:34:03 +01:00
Dave Hrycyszyn
f5da5af0b9 Bitcoin keys now being produced per-node 2024-06-18 16:32:32 +01:00
Dave Hrycyszyn
4cf6513959 Getting ready to create Bitcoin keys 2024-06-18 16:00:02 +01:00
Dave Hrycyszyn
a244207f77 ibid 2024-06-18 15:38:42 +01:00
Dave Hrycyszyn
97a4689a03 ibid 2024-06-18 15:35:22 +01:00
Dave Hrycyszyn
4451944b9e More README explanation about what needs to happen next 2024-06-18 15:33:38 +01:00
Dave Hrycyszyn
8375e4ce1e Did a bit of work on the README 2024-06-18 15:20:46 +01:00
Dave Hrycyszyn
fbf547ce0e Knocked down benchmark length 2024-06-18 12:25:07 +01:00
Dave Hrycyszyn
48a83fcd55 Removing unused crates 2024-06-18 11:59:17 +01:00
Dave Hrycyszyn
7d90f0653e Switched to Criterion for benchmarks 2024-06-18 11:52:48 +01:00
Dave Hrycyszyn
0372ac58b1 Fixed test import 2024-06-18 11:29:46 +01:00
Dave Hrycyszyn
3aee402a38 ibid 2024-06-18 11:25:38 +01:00
Dave Hrycyszyn
9837916874 Noted the problem with json field ordering 2024-06-18 11:24:21 +01:00
Dave Hrycyszyn
a4441af53a Fixed serializiation determinacy problems. 2024-06-18 11:19:36 +01:00
Dave Hrycyszyn
416d1ad88b WIP: hash inequality seems to be happening from something on the wire 2024-06-18 10:17:59 +01:00
Dave Hrycyszyn
e9870241cb Fixing unused imports 2024-06-17 15:54:26 +01:00
Dave Hrycyszyn
5a126845c4 Removing unused code 2024-06-17 15:52:03 +01:00
Dave Hrycyszyn
d1c18b6515 Tests working again 2024-06-17 15:43:09 +01:00
Dave Hrycyszyn
d38721e1a0 Splitting side-node crate into lib/bin for integration tests 2024-06-17 15:25:22 +01:00
Dave Hrycyszyn
8fa0eebe2b A bit of test variable renaming 2024-06-13 11:36:51 +01:00
Dave Hrycyszyn
324aaa109f Moving crdt tests into side-node so we can test serialization with real transactions 2024-06-13 11:35:07 +01:00
Dave Hrycyszyn
481b041554 Fixing test indeterminacy 2024-06-13 11:34:41 +01:00
Dave Hrycyszyn
7bb672f4b8 Applying multiple times leaves views equal 2024-06-12 15:07:24 +01:00
Dave Hrycyszyn
28e606ba51 Making note of the (finicky) watch run commands 2024-06-12 15:07:03 +01:00
Dave Hrycyszyn
0a74c86c5e Nearly workign 2024-06-11 19:16:36 +01:00
Dave Hrycyszyn
097fbea9a0 Consuming SignedOp when it's handled 2024-06-11 18:42:13 +01:00
Dave Hrycyszyn
546a45bb3a Deleting unused mod 2024-06-11 18:37:16 +01:00
Dave Hrycyszyn
950a63c103 Moved stdin_input to stdin::input module, pulling it out of main 2024-06-11 18:35:33 +01:00
Dave Hrycyszyn
f9c4fce398 Stopped double-encoding the SignedOp 2024-06-11 18:33:08 +01:00
Dave Hrycyszyn
014462c187 Using the ezsockets call methods to shoot text at the websocket. 2024-06-11 18:29:02 +01:00
Dave Hrycyszyn
b1daec3b84 Figured out what on_call is for 2024-06-11 18:13:51 +01:00
Dave Hrycyszyn
e0c991d0f9 Getting ready for network broadcast 2024-06-11 17:06:49 +01:00
Dave Hrycyszyn
a53b5bd94c Tidy 2024-06-11 16:52:40 +01:00
Dave Hrycyszyn
d13df41b82 Variable rename 2024-06-11 16:52:15 +01:00
Dave Hrycyszyn
4496a0916b Removing period send code 2024-06-11 16:51:13 +01:00
Dave Hrycyszyn
f3bea8c62d Restructuring tokio tasks and stdin receiver 2024-06-11 16:50:21 +01:00
Dave Hrycyszyn
443c4e1dac Simplifying 2024-06-10 16:43:45 +01:00
Dave Hrycyszyn
6077c3a519 Pinpointing blocking point 2024-06-10 16:33:03 +01:00
Dave Hrycyszyn
91fbe7f9bd Going back to blocking, need a new thread here 2024-06-10 14:26:00 +01:00
Dave Hrycyszyn
4717ffa7e8 Almost working, I've got a blocking I/O problem with stdin now :) 2024-06-10 14:25:05 +01:00
Dave Hrycyszyn
c3f5b2890b More pushing code around 2024-06-07 18:42:28 +01:00
Dave Hrycyszyn
9dc515fb78 Renamed write to write_toml in the config 2024-06-07 18:22:07 +01:00
Dave Hrycyszyn
6f756d4fb6 Minor cleanup 2024-06-07 18:20:02 +01:00
Dave Hrycyszyn
5d6a1e806a Nearly there 2024-06-07 17:35:38 +01:00
Dave Hrycyszyn
d91a631fdc More re-jigging 2024-06-07 17:18:46 +01:00
Dave Hrycyszyn
a81d1f913a Starting to modify things into container structs 2024-06-07 17:03:05 +01:00
Dave Hrycyszyn
b1f5d2b75a User serde_json for SignedOp serialization 2024-06-07 14:58:41 +01:00
Dave Hrycyszyn
95e3127903 Giving nodes the ability to send transactions in a more controlled fashion 2024-06-06 19:50:24 +01:00
Dave Hrycyszyn
3f4b4324e5 Implementing Display for SignedOp 2024-06-06 19:49:53 +01:00
Dave Hrycyszyn
404a769259 ezsockets integrated with cli startup 2024-06-06 19:32:29 +01:00
Dave Hrycyszyn
ff9fbd49ec Starting a move towards ezsockets 2024-06-06 19:25:54 +01:00
39 changed files with 2964 additions and 493 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/side.iml" filepath="$PROJECT_DIR$/.idea/side.iml" />
</modules>
</component>
</project>

17
.idea/side.iml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/crates/bft-json-crdt/benches" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/crates/bft-json-crdt/bft-crdt-derive/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/bft-json-crdt/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/bft-json-crdt/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/side-node/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/side-node/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/side-watcher/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

1800
Cargo.lock generated

File diff suppressed because it is too large Load Diff

77
README.md Normal file
View File

@@ -0,0 +1,77 @@
# Side BFT-CRDT PoC
This is a proof of concept implementation of a BFT-CRDT blockchain system.
## Running in development
Run the watcher first:
```bash
cd side-watcher
cargo watch -x run
```
To init a Side node:
```bash
cd side-node
cargo run -- init node1
cargo run -- init node2
cargo run -- init node3
cargo run -- init node4
```
To start a node with a cargo watch for development purposes (from the side-node dir), open up a few terminals and run:
```bash
cargo watch -x "run -- run -- node1"
cargo watch -x "run -- run -- node2"
cargo watch -x "run -- run -- node3"
cargo watch -x "run -- run -- node4"
```
## Discussion
What we have here is a very simple system comprised of two key parts: the Side Node, and the Side Watcher.
### Side Node(s)
The Side Nodes make up a system of BFT-CRDT-producing nodes that can make a blockchain. Currently they can reliably send transactions to each other in a secure way, such that all nodes they communicate with can tell whether received transactions are obeying the rules of the system.
Next dev tasks:
[ ] enable Side Nodes to download current P2P chain state so that they start out with a consistent copy of transaction data
[ ] add smart contract execution engine (CosmWasm would be a good first choice)
[ ] enable Side Nodes to download contract code for a given contract
[ ] enable Side Nodes to download current contract state for a given contract
[ ] switch to full P2P messaging instead of websockets
[ ] take the Side Watcher out of the system by electing a Side Node as a leader, so that agreement about transaction inclusion can be reached for a given block.
### Side Watcher
The Side Watcher is a simple relayer node that sits between the Side Chain (Cosmos) and the decentralized Side Nodes. At the moment, it simply relays transactions between nodes via a websocket. We aim to eliminate this component from the architecture, but for the moment it simplifies networking and consensus agreement while we experiment with higher-value concepts.
To fulfill the promises in the Lite Paper, the Side Watcher needs to:
[ ] make a block for the P2P when the Side Chain creates a block
[ ] submit P2P chain data to the Side Chain
Later, we will aim to remove the Side Watcher from the architecture, by (a) moving to pure P2P transactions between Side Nodes, and (b) doing leader election of a Side Node to reach agreement on the submitted block.
## Bitcoin integration
There is a Bitcoin client integrated into the node, which can do simple coin transfers using esplora and the Mutinynet server's Signet (30 second blocktime).
The client's demo driver can be run by doing:
```
cargo run -- init dave
cargo run -- init sammy
cargo run -- btc
```
You'll need to have funded the "dave" address prior to running the `btc` command - otherwise the transfer will fail gracefully.
I was using this primarily as a way to experiment with constructing and broadcasting Bitcoin transactions, with the hope that it would be possible to move on to more advanced constructions (e.g. state channels). However, now that I look at all the options, it seems that multi-party state channels in Bitcoin are (probably) impossible to construct.
There is a second, unused Bitcoin client in place which uses Blockstream's Electrum server, but this didn't seem to be working properly with respect to Signet Bitcoin network during my testing, so I went with the esplora / Mutiny version instead.

View File

@@ -17,15 +17,20 @@ bft = []
bft-crdt-derive = { path = "bft-crdt-derive" }
colored = "2.0.0"
fastcrypto = "0.1.8"
itertools = "0.10.5"
indexmap = { version = "2.2.6", features = ["serde"] }
rand = "0.8.5"
random_color = "0.6.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
serde_json = { version = "1.0.85", features = ["preserve_order"] }
serde_with = "3.8.1"
sha2 = "0.10.6"
[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
criterion = { version = "0.4", features = ["html_reports"] }
time = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0.85", features = ["preserve_order"] }
[[bench]]
name = "speed"
harness = false

View File

@@ -1,59 +1,67 @@
#![feature(test)]
extern crate test;
use bft_json_crdt::{
json_crdt::JsonValue, keypair::make_author, list_crdt::ListCrdt, op::Op, op::ROOT_ID,
};
use criterion::{criterion_group, criterion_main, Criterion};
use rand::seq::SliceRandom;
use test::Bencher;
#[bench]
fn bench_insert_1_000_root(b: &mut Bencher) {
b.iter(|| {
let mut list = ListCrdt::<i64>::new(make_author(1), vec![]);
for i in 0..1_000 {
list.insert(ROOT_ID, i);
}
})
}
#[bench]
fn bench_insert_1_000_linear(b: &mut Bencher) {
b.iter(|| {
let mut list = ListCrdt::<i64>::new(make_author(1), vec![]);
let mut prev = ROOT_ID;
for i in 0..1_000 {
let op = list.insert(prev, i);
prev = op.id;
}
})
}
#[bench]
fn bench_insert_many_agents_conflicts(b: &mut Bencher) {
b.iter(|| {
const N: u8 = 50;
let mut rng = rand::thread_rng();
let mut crdts: Vec<ListCrdt<i64>> = Vec::with_capacity(N as usize);
let mut logs: Vec<Op<JsonValue>> = Vec::new();
for i in 0..N {
let list = ListCrdt::new(make_author(i), vec![]);
crdts.push(list);
for _ in 0..5 {
let op = crdts[i as usize].insert(ROOT_ID, i as i32);
logs.push(op);
fn bench_insert_100_root(c: &mut Criterion) {
c.bench_function("bench insert 100 root", |b| {
b.iter(|| {
let mut list = ListCrdt::<i64>::new(make_author(1), vec![]);
for i in 0..100 {
list.insert(ROOT_ID, i);
}
}
})
});
}
logs.shuffle(&mut rng);
for op in logs {
for c in &mut crdts {
if op.author() != c.our_id {
c.apply(op.clone());
fn bench_insert_100_linear(c: &mut Criterion) {
c.bench_function("bench insert 100 linear", |b| {
b.iter(|| {
let mut list = ListCrdt::<i64>::new(make_author(1), vec![]);
let mut prev = ROOT_ID;
for i in 0..100 {
let op = list.insert(prev, i);
prev = op.id;
}
})
});
}
fn bench_insert_many_agents_conflicts(c: &mut Criterion) {
c.bench_function("bench insert many agents conflicts", |b| {
b.iter(|| {
const N: u8 = 10;
let mut rng = rand::thread_rng();
let mut crdts: Vec<ListCrdt<i64>> = Vec::with_capacity(N as usize);
let mut logs: Vec<Op<JsonValue>> = Vec::new();
for i in 0..N {
let list = ListCrdt::new(make_author(i), vec![]);
crdts.push(list);
for _ in 0..5 {
let op = crdts[i as usize].insert(ROOT_ID, i as i32);
logs.push(op);
}
}
}
assert!(crdts.windows(2).all(|w| w[0].view() == w[1].view()));
})
logs.shuffle(&mut rng);
for op in logs {
for c in &mut crdts {
if op.author() != c.our_id {
c.apply(op.clone());
}
}
}
assert!(crdts.windows(2).all(|w| w[0].view() == w[1].view()));
})
});
}
criterion_group!(
benches,
bench_insert_100_root,
bench_insert_100_linear,
bench_insert_many_agents_conflicts
);
criterion_main!(benches);

View File

@@ -8,6 +8,7 @@ publish = false
proc-macro = true
[dependencies]
indexmap = { version = "2.2.6", features = ["serde"] }
proc-macro2 = "1.0.47"
proc-macro-crate = "1.2.1"
quote = "1.0.21"

View File

@@ -6,13 +6,12 @@ use syn::{
parse::{self, Parser},
parse_macro_input,
spanned::Spanned,
Data, DeriveInput, Field, Fields, ItemStruct, LitStr, Type
Data, DeriveInput, Field, Fields, ItemStruct, LitStr, Type,
};
/// Helper to get tokenstream representing the parent crate
fn get_crate_name() -> TokenStream {
let cr8 = crate_name("bft-json-crdt")
.unwrap_or(FoundCrate::Itself);
let cr8 = crate_name("bft-json-bft-crdt").unwrap_or(FoundCrate::Itself);
match cr8 {
FoundCrate::Itself => quote! { ::bft_json_crdt },
FoundCrate::Name(name) => {
@@ -106,17 +105,23 @@ pub fn derive_json_crdt(input: OgTokenStream) -> OgTokenStream {
})
} else {
Err(format!("failed to convert {:?} -> {}<T>", value, #ident_str.to_string()))
}
}
}
}
impl #impl_generics std::fmt::Debug for #ident #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut fields = Vec::new();
#(fields.push(format!("{}", #ident_strings.to_string()));)*
write!(f, "{{ {:?} }}", fields.join(", "))
}
}
// I'm pulling this out so that we can see actual CRD content in debug output.
//
// The plan is to mostly get rid of the macros anyway, so it's a reasonable first step.
// It could (alternately) be just as good to keep the macros and change this function to
// output actual field content instead of just field names.
//
// impl #impl_generics std::fmt::Debug for #ident #ty_generics #where_clause {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// let mut fields = Vec::new();
// #(fields.push(format!("{}", #ident_strings.to_string()));)*
// write!(f, "{{ {:?} }}", fields.join(", "))
// }
// }
impl #impl_generics #crate_name::json_crdt::CrdtNode for #ident #ty_generics #where_clause {
fn apply(&mut self, op: #crate_name::op::Op<#crate_name::json_crdt::JsonValue>) -> #crate_name::json_crdt::OpState {
@@ -128,7 +133,7 @@ pub fn derive_json_crdt(input: OgTokenStream) -> OgTokenStream {
}
if self.path.len() == op.path.len() {
return #crate_name::json_crdt::OpState::ErrApplyOnStruct;
return #crate_name::json_crdt::OpState::ErrApplyOnStruct;
} else {
let idx = self.path.len();
if let #crate_name::op::PathSegment::Field(path_seg) = &op.path[idx] {
@@ -139,12 +144,12 @@ pub fn derive_json_crdt(input: OgTokenStream) -> OgTokenStream {
_ => {},
};
};
return #crate_name::json_crdt::OpState::ErrPathMismatch
return #crate_name::json_crdt::OpState::ErrPathMismatch
}
}
fn view(&self) -> #crate_name::json_crdt::JsonValue {
let mut view_map = std::collections::HashMap::new();
let mut view_map = indexmap::IndexMap::new();
#(view_map.insert(#ident_strings.to_string(), self.#ident_literals.view().into());)*
#crate_name::json_crdt::JsonValue::Object(view_map)
}
@@ -173,7 +178,7 @@ pub fn derive_json_crdt(input: OgTokenStream) -> OgTokenStream {
fn debug_view(&self, _indent: usize) -> String {
"".to_string()
}
}
}
};
// Hand the output tokens back to the compiler

View File

@@ -17,6 +17,13 @@ use fastcrypto::{
traits::{KeyPair, ToFromBytes},
// Verifier,
};
// TODO: serde's json object serialization and deserialization (correctly) do not define anything
// object field order in JSON objects. However, the hash check impl in bft-json-bft-crdt does take order
// into account. This is going to cause problems later for non-Rust implementations, BFT hash checking
// currently depends on JSON serialization/deserialization object order. This shouldn't be the case
// but I've hacked in an IndexMap for the moment to get the PoC working. To see the problem, replace this with
// a std HashMap, everything will screw up (annoyingly, only *most* of the time).
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, Bytes};
@@ -212,7 +219,7 @@ impl<T: CrdtNode + DebugView> BaseCrdt<T> {
/// Apply a signed operation to this BaseCRDT, verifying integrity and routing to the right
/// nested CRDT
pub fn apply(&mut self, op: SignedOp) -> OpState {
self.log_try_apply(&op);
// self.log_try_apply(&op);
#[cfg(feature = "bft")]
if !op.is_valid_digest() {
@@ -232,9 +239,9 @@ impl<T: CrdtNode + DebugView> BaseCrdt<T> {
}
// apply
self.log_actually_apply(&op);
// self.log_actually_apply(&op);
let status = self.doc.apply(op.inner);
self.debug_view();
// self.debug_view();
self.received.insert(op_id);
// apply all of its causal dependents if there are any
@@ -256,7 +263,7 @@ pub enum JsonValue {
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
Object(IndexMap<String, JsonValue>),
}
impl Display for JsonValue {
@@ -542,7 +549,7 @@ mod test {
#[test]
fn test_derive_basic() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Player {
x: LwwRegisterCrdt<f64>,
y: LwwRegisterCrdt<f64>,
@@ -557,14 +564,14 @@ mod test {
#[test]
fn test_derive_nested() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Position {
x: LwwRegisterCrdt<f64>,
y: LwwRegisterCrdt<f64>,
}
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Player {
pos: Position,
balance: LwwRegisterCrdt<f64>,
@@ -582,7 +589,7 @@ mod test {
#[test]
fn test_lww_ops() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Test {
a: LwwRegisterCrdt<f64>,
b: LwwRegisterCrdt<bool>,
@@ -642,7 +649,7 @@ mod test {
#[test]
fn test_vec_and_map_ops() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Test {
a: ListCrdt<String>,
}
@@ -682,14 +689,14 @@ mod test {
#[test]
fn test_causal_field_dependency() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Item {
name: LwwRegisterCrdt<String>,
soulbound: LwwRegisterCrdt<bool>,
}
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Player {
inventory: ListCrdt<Item>,
balance: LwwRegisterCrdt<f64>,
@@ -748,7 +755,7 @@ mod test {
#[test]
fn test_2d_grid() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Game {
grid: ListCrdt<ListCrdt<LwwRegisterCrdt<bool>>>,
}
@@ -809,7 +816,7 @@ mod test {
#[test]
fn test_arb_json() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Test {
reg: LwwRegisterCrdt<JsonValue>,
}
@@ -845,13 +852,13 @@ mod test {
#[test]
fn test_wrong_json_types() {
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Nested {
list: ListCrdt<f64>,
}
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Test {
reg: LwwRegisterCrdt<bool>,
strct: ListCrdt<Nested>,

View File

@@ -20,7 +20,7 @@ use serde_json::json;
// 5. block actual messages from honest actors (eclipse attack)
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct ListExample {
list: ListCrdt<char>,
}
@@ -91,13 +91,13 @@ fn test_forge_update() {
}
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Nested {
a: Nested2,
}
#[add_crdt_fields]
#[derive(Clone, CrdtNode)]
#[derive(Clone, CrdtNode, Debug)]
struct Nested2 {
b: LwwRegisterCrdt<bool>,
}

View File

@@ -259796,7 +259796,7 @@ function insertAt(idx, elt) {
const pos = new_log.findIndex(log => log[0] === parent_id)
new_log.push([ID_COUNTER, pos, 0, elt])
crdt.splice(raw_i + 1, 0, { deleted: false, content: elt, id: ID_COUNTER })
// console.log(`insert at ${idx} translated as op [${ID_COUNTER}, ${pos}, ${0}, ${escape(elt)}] found at ${raw_i + 1}::`, crdt[raw_i + 1])
// console.log(`insert at ${idx} translated as op [${ID_COUNTER}, ${pos}, ${0}, ${escape(elt)}] found at ${raw_i + 1}::`, bft-crdt[raw_i + 1])
return
}
@@ -259816,7 +259816,7 @@ function deleteAt(idx) {
const pos = new_log.findIndex(log => log[0] === our_id)
new_log.push([ID_COUNTER, pos, 1]);
crdt[raw_i].deleted = true
// console.log(`delete at ${idx} translated as op [${ID_COUNTER}, ${pos}, ${1}] found at ${raw_i} with our_id ${our_id}::`, crdt[raw_i])
// console.log(`delete at ${idx} translated as op [${ID_COUNTER}, ${pos}, ${1}] found at ${raw_i} with our_id ${our_id}::`, bft-crdt[raw_i])
return
}
@@ -259853,7 +259853,7 @@ function rawJSString(edits) {
// deleteAt(edit[0])
// }
// }
// console.log(crdt)
// console.log(bft-crdt)
// rawJSString(mock_edits)
// console.log(new_log)
// const subset = edits.slice(0, 50000)

7
side-node/Cargo.lock generated
View File

@@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "side-node"
version = "0.1.0"

View File

@@ -6,20 +6,34 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.21.7"
anyhow = "1.0.86"
async-trait = "0.1.52"
bdk = { version = "0.29.0", default-feature = false, features = ["all-keys"] }
bdk_esplora = "0.15.0"
bdk_sqlite = "0.2.0"
bdk_wallet = { version = "1.0.0-alpha.13", features = ["all-keys"] }
bft-json-crdt = { path = "../crates/bft-json-crdt" }
bft-crdt-derive = { path = "../crates/bft-json-crdt/bft-crdt-derive" }
bitcoin = { version = "0.32.2", features = ["rand"] }
clap = { version = "4.5.4", features = ["derive"] }
dirs = "5.0.1"
electrum-client = "0.20"
ezsockets = { version = "*", features = ["client"] }
fastcrypto = "0.1.8"
indexmap = { version = "2.2.6", features = ["serde"] }
reqwest = { version = "*", features = ["blocking"] }
# serde_cbor = "0.11.2" # move to this once we need to pack things in CBOR
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.117"
serde_with = "3.8.1"
sha256 = "1.5.0"
tokio = { version = "1.37.0", features = ["full"] }
websockets = "0.3.0"
toml = "0.8.14"
fastcrypto = "0.1.8"
tracing = "0.1.32"
# tracing-subscriber = "0.3.9"
[dev-dependencies]
uuid = { version = "1.8.0", features = ["v4"] }
[features]
default = ["bft", "logging-list", "logging-json"]

View File

@@ -8,7 +8,7 @@ use bft_json_crdt::keypair::{make_keypair, Ed25519KeyPair};
use fastcrypto::traits::EncodeDecodeBase64;
/// Writes a new Ed25519 keypair to the file at key_path.
pub(crate) fn write(key_path: PathBuf) -> Result<(), std::io::Error> {
pub(crate) fn write(key_path: &PathBuf) -> Result<(), std::io::Error> {
let keys = make_keypair();
let mut file = File::create(key_path)?;
@@ -17,10 +17,10 @@ pub(crate) fn write(key_path: PathBuf) -> Result<(), std::io::Error> {
Ok(())
}
pub(crate) fn load_from_file(side_dir: PathBuf) -> Ed25519KeyPair {
pub(crate) fn load_from_file(side_dir: &PathBuf) -> Ed25519KeyPair {
let key_path = crate::utils::side_paths(side_dir.clone()).0;
let data = fs::read_to_string(key_path).expect("couldn't read key file");
let data = fs::read_to_string(key_path).expect("couldn't read bft-bft-crdt key file");
println!("data: {:?}", data);
Ed25519KeyPair::decode_base64(&data).expect("couldn't load keypair from file")

View File

@@ -0,0 +1,30 @@
use bft_crdt_derive::add_crdt_fields;
use bft_json_crdt::{
json_crdt::{CrdtNode, IntoCrdtNode},
list_crdt::ListCrdt,
};
use serde::{Deserialize, Serialize};
pub mod keys;
pub mod websocket;
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Serialize, Deserialize, Debug)]
pub struct TransactionList {
pub list: ListCrdt<Transaction>,
}
impl TransactionList {
pub fn view_sha(&self) -> String {
sha256::digest(serde_json::to_string(&self.list.view()).unwrap().as_bytes()).to_string()
}
}
/// A fake Transaction struct we can use as a simulated payload
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Serialize, Deserialize, PartialEq, Debug)]
pub struct Transaction {
from: String,
to: String,
amount: f64,
}

View File

@@ -0,0 +1,66 @@
use async_trait::async_trait;
use bft_json_crdt::json_crdt::SignedOp;
use ezsockets::ClientConfig;
use tokio::sync::mpsc;
use crate::utils;
pub struct Client {
incoming_sender: mpsc::Sender<SignedOp>,
handle: ezsockets::Client<Client>,
}
impl Client {
/// Start the websocket client
pub async fn new(incoming_sender: mpsc::Sender<SignedOp>) -> ezsockets::Client<Client> {
let config = ClientConfig::new("ws://localhost:8080/websocket");
let (handle, future) = ezsockets::connect(
|client| Client {
incoming_sender,
handle: client,
},
config,
)
.await;
tokio::spawn(async move {
future.await.unwrap();
});
handle
}
}
#[async_trait]
impl ezsockets::ClientExt for Client {
// Right now we're only using the Call type for sending signed ops
// change this to an enum if we need to send other types of calls, and
// match on it.
type Call = String;
/// When we receive a text message, apply the bft-crdt operation contained in it to our
/// local bft-crdt.
async fn on_text(&mut self, text: String) -> Result<(), ezsockets::Error> {
let string_sha = utils::shassy(text.clone());
println!("received text, sha: {string_sha}");
let incoming: bft_json_crdt::json_crdt::SignedOp = serde_json::from_str(&text).unwrap();
let object_sha = utils::shappy(incoming.clone());
println!("deserialized: {}", object_sha);
if string_sha != object_sha {
panic!("sha mismatch: {string_sha} != {object_sha}, bft-bft-crdt has failed");
}
self.incoming_sender.send(incoming).await?;
Ok(())
}
/// When we receive a binary message, log the bytes. Currently unused.
async fn on_binary(&mut self, bytes: Vec<u8>) -> Result<(), ezsockets::Error> {
tracing::info!("received bytes: {bytes:?}");
Ok(())
}
/// Call this with the `Call` type to send application data to the websocket client
/// (and from there, to the server).
async fn on_call(&mut self, call: Self::Call) -> Result<(), ezsockets::Error> {
self.handle.text(call)?;
Ok(())
}
}

View File

@@ -0,0 +1,101 @@
use crate::{bitcoin, utils};
use bdk::bitcoin::psbt::PartiallySignedTransaction;
use bdk::bitcoin::Network;
use bdk::database::MemoryDatabase;
use bdk::keys::ExtendedKey;
use bdk::template::Bip84;
use bdk::wallet::AddressIndex::{self, New};
use bdk::wallet::AddressInfo;
use bdk::{blockchain::ElectrumBlockchain, electrum_client, SyncOptions};
use bdk::{FeeRate, KeychainKind, SignOptions, TransactionDetails, Wallet};
/// DEPRECATED
///
/// This is a bdk example that uses the Electrum client to interact with the Bitcoin network.
/// Electrum is a light client that connects to a server to get information about the Bitcoin network.
/// The BDK itself does not have the ability to connect to e.g. esplora servers. As the Blockstream Electrum Signet
/// server does not appear to be picking up transactions properly at the moment, I've shifted over to using
/// the (more complex) `bdk_wallet` crate and the esplora client there (see the other bitcoin client).
///
/// Note:the types below are all completely different than the types in `bdk_wallet`.
pub async fn run() -> Result<(), anyhow::Error> {
let dave = utils::home(&"dave".to_string());
let sammy = utils::home(&"sammy".to_string());
let dave_key = bitcoin::keys::load_from_file(&dave).unwrap();
let sammy_key = bitcoin::keys::load_from_file(&sammy).unwrap();
let dave_wallet = create_wallet(dave_key)?;
let sammy_wallet = create_wallet(sammy_key)?;
let dave_address = dave_wallet.get_address(AddressIndex::Peek(0))?.to_string();
let sammy_address = sammy_wallet.get_address(AddressIndex::Peek(0))?.to_string();
println!("Dave's address: {}", dave_address);
println!("Sammy's address: {}", sammy_address);
let blockchain = ElectrumBlockchain::from(electrum_client::Client::new(
"ssl://electrum.blockstream.info:60002",
)?);
println!("Syncing...");
dave_wallet.sync(&blockchain, SyncOptions::default())?;
display_balance(&dave_wallet);
display_balance(&sammy_wallet);
let (mut psbt, details) =
build_sending_tx(&dave_wallet, sammy_wallet.get_address(New)?).expect("psbt build error");
println!("About to sign the transaction: {:?}", details);
dave_wallet.sign(&mut psbt, SignOptions::default())?;
let _signed_tx = psbt.extract_tx();
// println!("Broadcasting...");
// blockchain.broadcast(&signed_tx).expect("broadcast error");
// println!("Transaction ID: {:?}", signed_tx.txid());
Ok(())
}
/// Create a BDK wallet using BIP 84 descriptor ("m/84h/1h/0h/0" and "m/84h/1h/0h/1")
pub fn create_wallet(xkey: ExtendedKey) -> anyhow::Result<Wallet<MemoryDatabase>> {
let xprv = xkey
.into_xprv(Network::Testnet)
.expect("couldn't turn xkey into xprv");
let external_descriptor = Bip84(xprv, KeychainKind::External);
let internal_descriptor = Some(Bip84(xprv, KeychainKind::Internal));
let wallet = Wallet::new(
external_descriptor,
internal_descriptor,
Network::Testnet,
MemoryDatabase::default(),
)?;
Ok(wallet)
}
fn display_balance(wallet: &Wallet<MemoryDatabase>) {
println!(
"Wallet balance for {} after syncing: {:?} sats on network {}",
wallet
.get_address(bdk::wallet::AddressIndex::Peek(0))
.expect("couldn't get address"),
wallet.get_balance().expect("couldn't show balance"),
wallet.network(),
);
}
fn build_sending_tx(
wallet: &Wallet<MemoryDatabase>,
recipient: AddressInfo,
) -> anyhow::Result<(PartiallySignedTransaction, TransactionDetails), anyhow::Error> {
let mut builder = wallet.build_tx();
builder
.add_recipient(recipient.script_pubkey(), 1000)
.enable_rbf()
.do_not_spend_change()
.fee_rate(FeeRate::from_sat_per_vb(7.0));
Ok(builder.finish()?)
}

View File

@@ -0,0 +1,188 @@
use std::{collections::BTreeSet, fs, io::Write};
use bdk::keys::bip39::Mnemonic;
use bdk_esplora::{
esplora_client::{self, AsyncClient},
EsploraAsyncExt,
};
use bdk_wallet::{
bitcoin::{Address, Amount, Network, Script},
chain::ConfirmationTimeHeightAnchor,
keys::{DerivableKey, ExtendedKey},
wallet::AddressInfo,
KeychainKind, SignOptions, Wallet,
};
use bdk_sqlite::{rusqlite::Connection, Store};
use crate::utils;
const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5;
/// A wallet that uses the Esplora client to interact with the Bitcoin network.
pub struct EsploraWallet {
client: AsyncClient,
db: Store<KeychainKind, ConfirmationTimeHeightAnchor>,
name: String,
wallet: Wallet,
}
impl EsploraWallet {
/// Builds and signs a send transaction to send coins between addresses.
///
/// Does NOT send it, you must call `broadcast` to do that.
///
/// We could split the creation and signing easily if needed.
pub(crate) fn build_and_sign_send_tx(
&mut self,
recipient: Address,
amount: Amount,
) -> Result<bitcoin::Transaction, anyhow::Error> {
let mut tx_builder = self.wallet.build_tx();
tx_builder
.add_recipient(recipient.script_pubkey(), amount)
.enable_rbf();
let mut psbt = tx_builder.finish()?;
let finalized = self.wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized);
let tx = psbt.extract_tx()?;
Ok(tx)
}
/// Syncs the wallet with the latest state of the Bitcoin blockchain
pub(crate) async fn sync(&mut self) -> Result<(), anyhow::Error> {
print!("Syncing...");
fn generate_inspect(
kind: KeychainKind,
) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
let mut once = Some(());
let mut stdout = std::io::stdout();
move |spk_i, _| {
match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", kind),
None => print!(" {:<3}", spk_i),
};
stdout.flush().expect("must flush");
}
}
let request = self
.wallet
.start_full_scan()
.inspect_spks_for_all_keychains({
let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| {
match once.insert(keychain) {
true => print!("\nScanning keychain [{:?}]", keychain),
false => print!(" {:<3}", spk_i),
}
std::io::stdout().flush().expect("must flush")
}
})
.inspect_spks_for_keychain(
KeychainKind::External,
generate_inspect(KeychainKind::External),
)
.inspect_spks_for_keychain(
KeychainKind::Internal,
generate_inspect(KeychainKind::Internal),
);
let mut update = self
.client
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
.await?;
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let _ = update.graph_update.update_last_seen_unconfirmed(now);
self.wallet.apply_update(update)?;
self.persist_local()?;
println!("Sync complete for {}", self.name);
Ok(())
}
fn persist_local(&mut self) -> Result<(), anyhow::Error> {
Ok(if let Some(changeset) = self.wallet.take_staged() {
self.db.write(&changeset)?;
})
}
/// Gets the next unused address from the wallet.
pub(crate) fn next_unused_address(&mut self) -> Result<AddressInfo, anyhow::Error> {
let address = self.wallet.next_unused_address(KeychainKind::External);
self.persist_local()?;
println!(
"Generated address: https://mutinynet.com/address/{}",
address
);
Ok(address)
}
/// Returns the balance of the wallet.
pub(crate) fn balance(&self) -> bdk_wallet::wallet::Balance {
self.wallet.balance()
}
/// Broadcasts a signed transaction to the network.
pub(crate) async fn broadcast(
&self,
tx: &bitcoin::Transaction,
) -> Result<(), esplora_client::Error> {
println!(
"{} broadcasting tx https://mutinynet.com/tx/{}",
self.name,
tx.compute_txid()
);
self.client.broadcast(tx).await
}
}
/// Creates a Bitcoin descriptor wallet with the mnemonic in the given user directory.
pub(crate) fn create_wallet(name: &str, network: Network) -> anyhow::Result<EsploraWallet> {
let keys_dir = utils::home(name);
let mnemonic_path = crate::utils::side_paths(keys_dir).1; // TODO: this tuple stinks
let mnemonic_words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file");
println!("Creating wallet from mnemonic: {mnemonic_words}");
let mnemonic = Mnemonic::parse(mnemonic_words).unwrap();
// Generate the extended key
let xkey: ExtendedKey = mnemonic
.into_extended_key()
.expect("couldn't turn mnemonic into xkey");
let xprv = xkey
.into_xprv(Network::Signet)
.expect("problem converting xkey to xprv")
.to_string();
println!("Setting up esplora database for {name}");
let db_path = format!("/tmp/{name}-bdk-esplora-async-example.sqlite");
let conn = Connection::open(db_path)?;
let mut db = Store::new(conn)?;
let external_descriptor = format!("wpkh({xprv}/84'/1'/0'/0/*)");
let internal_descriptor = format!("wpkh({xprv}/84'/1'/0'/1/*)");
let changeset = db.read().expect("couldn't read esplora database");
let wallet = Wallet::new_or_load(
&external_descriptor,
&internal_descriptor,
changeset,
network,
)
.expect("problem setting up wallet");
let client = esplora_client::Builder::new("https://mutinynet.com/api")
.build_async()
.expect("couldn't build esplora client");
let esplora = EsploraWallet {
name: name.to_string(),
wallet,
db,
client,
};
Ok(esplora)
}

View File

@@ -0,0 +1,2 @@
pub mod electrum;
pub mod esplora;

View File

@@ -0,0 +1,62 @@
use std::str::FromStr;
use bdk_wallet::bitcoin::{Address, Amount, Network};
use crate::bitcoin::clients;
/// Demonstrates the use of bdk with the Esplora client.
///
/// This is more complex than the bare `bdk` crate, but the esplora client works.
///
/// Also, it very handily works with the mutinynet.com esplora server, which is configured
/// with 30 second block times.
pub(crate) async fn run() -> Result<(), anyhow::Error> {
simple_transfer().await
}
async fn simple_transfer() -> Result<(), anyhow::Error> {
let mut dave = clients::esplora::create_wallet("dave", Network::Signet)?;
let mut sammy = clients::esplora::create_wallet("sammy", Network::Signet)?;
let _next_address = dave.next_unused_address()?;
let dave_balance = dave.balance();
println!(
"Dave wallet balance before syncing: {} sats",
dave_balance.total()
);
dave.sync().await?;
let dave_balance = dave.balance();
println!("Wallet balance after syncing: {} sats", dave_balance);
let sammy_address = sammy.next_unused_address()?.address;
println!("Sammy's address: {}", sammy_address);
let sammy_balance = sammy.balance();
println!(
"Sammy wallet balance before syncing: {} sats",
sammy_balance
);
sammy.sync().await?;
let sammy_balance = sammy.balance();
println!("Sammy wallet balance after syncing: {} sats", sammy_balance);
let send_amount = Amount::from_sat(500);
if dave_balance.total() < send_amount {
println!(
"Please send at least {} sats to the receiving address",
send_amount
);
std::process::exit(0);
}
let tx = dave.build_and_sign_send_tx(sammy_address, send_amount)?;
dave.broadcast(&tx).await?;
Ok(())
}

View File

@@ -0,0 +1,50 @@
use bdk::{
keys::{
bip39::{Language, Mnemonic, WordCount},
DerivableKey, ExtendedKey, GeneratableKey, GeneratedKey,
},
miniscript,
};
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
};
pub fn make_mnemonic() -> String {
let mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
Mnemonic::generate((WordCount::Words12, Language::English)).unwrap();
mnemonic.to_string()
}
/// Write the mnemonic to a file in the node's side directory
///
/// TODO: obviously spitting the mnemonic out to the console is not for production
pub(crate) fn write(mnemonic_path: &PathBuf) -> Result<(), std::io::Error> {
let mnemonic = make_mnemonic();
let mut file = File::create(mnemonic_path)?;
println!("mnemonic: {mnemonic}");
file.write(mnemonic.as_bytes())?;
Ok(())
}
/// Creates Signet Bitcoin descriptors from a mnemonic
pub fn get(mnemonic_words: String) -> anyhow::Result<ExtendedKey> {
let mnemonic = Mnemonic::parse(mnemonic_words).unwrap();
// Generate the extended key
let xkey: ExtendedKey = mnemonic
.into_extended_key()
.expect("couldn't turn mnemonic into xkey");
Ok(xkey)
}
pub(crate) fn load_from_file(side_dir: &PathBuf) -> anyhow::Result<ExtendedKey> {
let mnemonic_path = crate::utils::side_paths(side_dir.clone()).1; // TODO: this tuple stinks
let mnemonic_words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file");
println!("Creating wallet from mnemonic: {mnemonic_words}");
get(mnemonic_words)
}

View File

@@ -0,0 +1,3 @@
pub mod clients;
pub mod driver;
pub mod keys;

View File

@@ -17,6 +17,9 @@ pub(crate) struct Args {
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Placeholder for future BTC commands
Btc {},
/// runs the Side Node
Run { name: String },

View File

@@ -10,7 +10,7 @@ pub(crate) struct SideNodeConfig {
pub(crate) name: String,
}
pub(crate) fn write(
pub(crate) fn write_toml(
config: &SideNodeConfig,
file_path: &PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {

View File

@@ -2,25 +2,28 @@ use std::path::PathBuf;
use config::SideNodeConfig;
use crate::{keys, utils};
use crate::{bft_crdt, bitcoin, utils};
pub(crate) mod config;
pub(crate) fn init(home: PathBuf, config: SideNodeConfig) -> Result<(), std::io::Error> {
ensure_side_directory_exists(&home)?;
let (key_path, config_path) = utils::side_paths(home.clone());
let (bft_crdt_key_path, bitcoin_key_path, config_path) = utils::side_paths(home.clone());
println!("Writing key to: {:?}", key_path);
keys::write(key_path)?;
println!("Writing bft bft-crdt key to: {:?}", bft_crdt_key_path);
bft_crdt::keys::write(&bft_crdt_key_path)?;
println!("Writing bitcoin key to: {:?}", bitcoin_key_path);
bitcoin::keys::write(&bitcoin_key_path)?;
println!("Writing config to: {:?}", config_path);
config::write(&config, &config_path).expect("unable to write config file");
config::write_toml(&config, &config_path).expect("unable to write config file");
Ok(())
}
/// Ensures that the directory at side_dir exists, so we have a place
/// to store our key file and config file.
/// to store our key files and config file.
fn ensure_side_directory_exists(side_dir: &PathBuf) -> Result<(), std::io::Error> {
if side_dir.exists() {
return Ok(());
@@ -34,7 +37,7 @@ fn ensure_side_directory_exists(side_dir: &PathBuf) -> Result<(), std::io::Error
#[cfg(test)]
mod tests {
use std::{fs, path::Path};
use std::{fs, path::Path, str::FromStr};
use fastcrypto::{
ed25519::Ed25519KeyPair,
@@ -43,50 +46,75 @@ mod tests {
use super::*;
fn default_side_node_config() -> SideNodeConfig {
SideNodeConfig {
name: "alice".to_string(),
}
/// Generates a SideNodeConfig with a unique name for each test.
/// This is necessary because the tests run in parallel and we
/// don't want them to interfere with each other - without a unique
/// name, the tests would all try to write to the same directory and we
/// get test indeterminacy
fn side_node_config() -> (SideNodeConfig, String) {
let name = format!("test-{}", uuid::Uuid::new_v4()).to_string();
(SideNodeConfig { name: name.clone() }, name)
}
#[test]
fn creates_bitcoin_keys() {
let (config, name) = side_node_config();
let side_dir = format!("/tmp/side/{name}");
let mut bitcoin_keys_path = PathBuf::new();
bitcoin_keys_path.push(side_dir.clone());
bitcoin_keys_path.push(utils::BITCOIN_KEY_FILE);
let _ = init(PathBuf::from_str(&side_dir).unwrap(), config);
assert!(bitcoin_keys_path.exists());
// check that the pem is readable
// let data = fs::read_to_string(bitcoin_keys_path).expect("couldn't read key file");
// let keys = Ed25519KeyPair::decode_base64(&data).expect("couldn't load keypair from file");
// assert_eq!(keys.public().as_bytes().len(), 32);
}
#[test]
fn creates_side_node_directory() {
let mut test_home = PathBuf::new();
let side_dir = "/tmp/side";
let (config, name) = side_node_config();
let side_dir = format!("/tmp/side/{name}");
// clean up any previous test runs
fs::remove_dir_all(side_dir).expect("couldn't remove side directory during test");
let mut test_home = PathBuf::new();
test_home.push(side_dir);
let node_dir = Path::new(&test_home).parent().unwrap().to_str().unwrap();
let _ = init(test_home.clone(), default_side_node_config());
let _ = init(test_home.clone(), config);
assert!(std::path::Path::new(node_dir).exists());
}
#[test]
fn creates_key_file() {
let mut file_path = PathBuf::new();
file_path.push("/tmp/side");
let side_dir = file_path.clone();
file_path.push(utils::KEY_FILE);
fn creates_bft_crdt_key_file() {
let (config, name) = side_node_config();
let side_dir = format!("/tmp/side/{name}");
let _ = init(side_dir.clone(), default_side_node_config());
assert!(file_path.exists());
let mut key_file_path = PathBuf::new();
key_file_path.push(side_dir.clone());
key_file_path.push(utils::BFT_CRDT_KEY_FILE);
let _ = init(PathBuf::from_str(&side_dir).unwrap(), config);
assert!(key_file_path.exists());
// check that the pem is readable
let data = fs::read_to_string(file_path).expect("couldn't read key file");
let data = fs::read_to_string(key_file_path).expect("couldn't read key file");
let keys = Ed25519KeyPair::decode_base64(&data).expect("couldn't load keypair from file");
assert_eq!(keys.public().as_bytes().len(), 32);
}
#[test]
fn creates_config_file() {
let mut file_path = PathBuf::new();
file_path.push("/tmp/side");
let side_dir = file_path.clone();
file_path.push(utils::CONFIG_FILE);
let (config, name) = side_node_config();
let side_dir = format!("/tmp/side/{name}");
let _ = init(side_dir.clone(), default_side_node_config());
assert!(file_path.exists());
let mut config_file_path = PathBuf::new();
config_file_path.push(side_dir.clone());
config_file_path.push(utils::CONFIG_FILE);
let _ = init(PathBuf::from_str(&side_dir).unwrap(), config);
assert!(config_file_path.exists());
}
}

67
side-node/src/lib.rs Normal file
View File

@@ -0,0 +1,67 @@
use bft_crdt::websocket;
use bft_crdt::TransactionList;
use bft_json_crdt::json_crdt::{BaseCrdt, SignedOp};
use cli::{parse_args, Commands};
use node::SideNode;
use tokio::{sync::mpsc, task};
pub mod bft_crdt;
pub mod bitcoin;
pub(crate) mod cli;
pub(crate) mod init;
pub mod node;
pub(crate) mod stdin;
pub mod utils;
#[tokio::main]
pub async fn run() {
let args = parse_args();
match &args.command {
Some(Commands::Init { name }) => {
let config = init::config::SideNodeConfig {
name: name.to_string(),
};
let _ = init::init(utils::home(name), config);
}
Some(Commands::Run { name }) => {
let mut node = setup(name).await;
node.start().await;
}
Some(Commands::Btc {}) => {
let _ = bitcoin::driver::run().await;
}
None => println!("No command provided. Exiting. See --help for more information."),
}
}
/// Wire everything up outside the application so that we can test more easily later
async fn setup(name: &String) -> SideNode {
// First, load up the keys and create a bft-bft-crdt
let side_dir = utils::home(name);
let bft_crdt_keys = bft_crdt::keys::load_from_file(&side_dir);
let keys = bitcoin::keys::load_from_file(&side_dir).unwrap();
let bitcoin_wallet = bitcoin::clients::electrum::create_wallet(keys).unwrap();
let crdt = BaseCrdt::<TransactionList>::new(&bft_crdt_keys);
// Channels for internal communication, and a tokio task for stdin input
let (incoming_sender, incoming_receiver) = mpsc::channel::<SignedOp>(32);
let (stdin_sender, stdin_receiver) = std::sync::mpsc::channel();
task::spawn(async move {
stdin::input(stdin_sender);
});
// Finally, create the node and return it
let handle = websocket::Client::new(incoming_sender).await;
let node = SideNode::new(
crdt,
bft_crdt_keys,
bitcoin_wallet,
incoming_receiver,
stdin_receiver,
handle,
);
println!("Node setup complete.");
node
}

View File

@@ -1,67 +0,0 @@
use std::path::PathBuf;
use bft_crdt_derive::add_crdt_fields;
use bft_json_crdt::{
json_crdt::{BaseCrdt, CrdtNode, IntoCrdtNode},
keypair::{Ed25519KeyPair, KeyPair},
list_crdt::ListCrdt,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use websockets::WebSocket;
use crate::keys;
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Serialize, Deserialize)]
pub(crate) struct CrdtList {
pub(crate) list: ListCrdt<Transaction>, // switch to Transaction as soon as char is working
}
/// A fake Transaction struct we can use as a simulated payload
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Serialize, Deserialize)]
pub(crate) struct Transaction {
from: String,
to: String,
amount: f64,
}
pub(crate) fn new(side_dir: PathBuf) -> (BaseCrdt<CrdtList>, Ed25519KeyPair) {
let keys = keys::load_from_file(side_dir);
let bft_crdt = BaseCrdt::<CrdtList>::new(&keys);
println!("Author is {}", keys.public().to_string());
(bft_crdt, keys)
}
pub(crate) async fn send(
count: u32,
bft_crdt: &mut BaseCrdt<CrdtList>,
ws: &mut WebSocket,
keys: &Ed25519KeyPair,
) -> Result<(), websockets::WebSocketError> {
// generate a placeholder transaction
let transaction = generate_transaction(count, keys.public().to_string());
// next job is to keep adding to this guy
let next = bft_crdt.doc.list.ops.len();
let signed_op = bft_crdt
.doc
.list
.insert_idx(next - 1, transaction.clone())
.sign(&keys);
Ok(ws
.send_text(serde_json::to_string(&signed_op).unwrap())
.await?)
}
fn generate_transaction(count: u32, pubkey: String) -> Value {
json!({
"from": pubkey,
"to": "Bob",
"amount": count
})
}

View File

@@ -1,36 +1,5 @@
use cli::{parse_args, Commands};
use side_node;
pub(crate) mod cli;
pub(crate) mod init;
pub(crate) mod keys;
pub(crate) mod list_transaction_crdt;
pub(crate) mod utils;
pub(crate) mod websocket;
#[tokio::main]
async fn main() {
let args = parse_args();
match &args.command {
Some(Commands::Init { name }) => {
let config = init::config::SideNodeConfig {
name: name.to_string(),
};
let _ = init::init(home(name), config);
}
Some(Commands::Run { name }) => {
let side_dir = home(name);
let (mut bft_crdt, keys) = list_transaction_crdt::new(side_dir);
websocket::start(keys, &mut bft_crdt).await.unwrap();
}
None => println!("No command provided. Exiting. See --help for more information."),
}
}
fn home(name: &String) -> std::path::PathBuf {
let mut path = dirs::home_dir().unwrap();
path.push(".side");
path.push(name);
path
fn main() {
side_node::run();
}

100
side-node/src/node.rs Normal file
View File

@@ -0,0 +1,100 @@
use bdk::database::MemoryDatabase;
use bft_json_crdt::json_crdt::{BaseCrdt, SignedOp};
use fastcrypto::ed25519::Ed25519KeyPair;
use tokio::sync::mpsc;
use crate::{bft_crdt::websocket::Client, bft_crdt::TransactionList, utils};
pub struct SideNode {
crdt: BaseCrdt<TransactionList>,
bft_crdt_keys: fastcrypto::ed25519::Ed25519KeyPair,
bitcoin_wallet: bdk::Wallet<MemoryDatabase>,
incoming_receiver: mpsc::Receiver<SignedOp>,
stdin_receiver: std::sync::mpsc::Receiver<String>,
handle: ezsockets::Client<Client>,
}
impl SideNode {
pub fn new(
crdt: BaseCrdt<TransactionList>,
bft_crdt_keys: Ed25519KeyPair,
bitcoin_wallet: bdk::Wallet<MemoryDatabase>,
incoming_receiver: mpsc::Receiver<SignedOp>,
stdin_receiver: std::sync::mpsc::Receiver<String>,
handle: ezsockets::Client<Client>,
) -> Self {
let node = Self {
crdt,
bft_crdt_keys,
bitcoin_wallet,
incoming_receiver,
stdin_receiver,
handle,
};
node
}
pub(crate) async fn start(&mut self) {
println!("Starting node...");
loop {
match self.stdin_receiver.try_recv() {
Ok(stdin) => {
let transaction = utils::fake_generic_transaction_json(stdin);
let json = serde_json::to_value(transaction).unwrap();
let signed_op = self.add_transaction_local(json);
println!("STDIN: {}", utils::shappy(signed_op.clone()));
self.send_to_network(signed_op).await;
}
Err(_) => {} // ignore empty channel errors in this PoC
}
match self.incoming_receiver.try_recv() {
Ok(incoming) => {
println!("INCOMING: {}", utils::shappy(incoming.clone()));
self.handle_incoming(incoming);
}
Err(_) => {} // ignore empty channel errors in this PoC
}
}
}
async fn send_to_network(&self, signed_op: SignedOp) {
let to_send = serde_json::to_string(&signed_op).unwrap();
self.handle.call(to_send).unwrap();
}
pub fn handle_incoming(&mut self, incoming: SignedOp) {
self.crdt.apply(incoming);
// self.trace_crdt();
}
pub fn add_transaction_local(
&mut self,
transaction: serde_json::Value,
) -> bft_json_crdt::json_crdt::SignedOp {
let last = self
.crdt
.doc
.list
.ops
.last()
.expect("couldn't find last op");
let signed_op = self
.crdt
.doc
.list
.insert(last.id, transaction)
.sign(&self.bft_crdt_keys);
// self.trace_crdt();
signed_op
}
/// Print the current state of the CRDT, can be used to debug
pub fn trace_crdt(&self) {
println!("{:?}", self.crdt.doc.view_sha());
}
pub fn current_sha(&self) -> String {
self.crdt.doc.view_sha()
}
}

11
side-node/src/stdin.rs Normal file
View File

@@ -0,0 +1,11 @@
use std::io::BufRead;
/// Wait for stdin terminal input and send it to the node if any arrives
pub(crate) fn input(stdin_sender: std::sync::mpsc::Sender<String>) {
let stdin = std::io::stdin();
let lines = stdin.lock().lines();
for line in lines {
let line = line.unwrap();
stdin_sender.send(line).unwrap();
}
}

View File

@@ -1,15 +1,48 @@
use bft_json_crdt::json_crdt::SignedOp;
use serde_json::{json, Value};
use std::path::PathBuf;
pub(crate) const KEY_FILE: &str = "keys.pem";
pub(crate) const BITCOIN_KEY_FILE: &str = "bitcoin_keys.pem";
pub(crate) const BFT_CRDT_KEY_FILE: &str = "keys.pem";
pub(crate) const CONFIG_FILE: &str = "config.toml";
/// Returns the path to the key file for this host OS.
pub(crate) fn side_paths(prefix: PathBuf) -> (PathBuf, PathBuf) {
let mut key_path = prefix.clone();
key_path.push(KEY_FILE);
/// Returns the path to the key file and config for this host OS.
pub(crate) fn side_paths(prefix: PathBuf) -> (PathBuf, PathBuf, PathBuf) {
let mut bft_crdt_key_path = prefix.clone();
bft_crdt_key_path.push(BFT_CRDT_KEY_FILE);
let mut bitcoin_key_path = prefix.clone();
bitcoin_key_path.push(BITCOIN_KEY_FILE);
let mut config_path = prefix.clone();
config_path.push(CONFIG_FILE);
(key_path, config_path)
(bft_crdt_key_path, bitcoin_key_path, config_path)
}
/// Returns the path to the home directory for this host OS and the given node name
pub(crate) fn home(name: &str) -> std::path::PathBuf {
let mut path = dirs::home_dir().unwrap();
path.push(".side");
path.push(name);
path
}
/// Generate a fake transaction with customizable from_pubkey String
pub fn fake_generic_transaction_json(from: String) -> Value {
json!({
"from": from,
"to": "Bob",
"amount": 1
})
}
pub fn shappy(op: SignedOp) -> String {
let b = serde_json::to_string(&op).unwrap().into_bytes();
sha256::digest(b).to_string()
}
pub fn shassy(text: String) -> String {
let b = text.into_bytes();
sha256::digest(b).to_string()
}

View File

@@ -1,41 +0,0 @@
use crate::list_transaction_crdt::{self, CrdtList};
use base64::{engine::general_purpose, Engine as _};
use bft_json_crdt::json_crdt::BaseCrdt;
use bft_json_crdt::json_crdt::SignedOp;
use bft_json_crdt::keypair::Ed25519KeyPair;
use tokio::time;
use websockets::WebSocket;
/// Starts a websocket and periodically sends a BFT-CRDT message to the websocket server
pub(crate) async fn start(
keys: Ed25519KeyPair,
bft_crdt: &mut BaseCrdt<CrdtList>,
) -> Result<(), websockets::WebSocketError> {
println!("connecting to websocket at ws://127.0.0.1:8080/");
let mut ws = WebSocket::connect("ws://127.0.0.1:8080/").await?;
let mut interval = every_ten_seconds();
let mut count = 0;
loop {
let _ = list_transaction_crdt::send(count, bft_crdt, &mut ws, &keys).await;
let msg = ws.receive().await?;
// deserialize the received websocket Frame into a string
let msg = msg.into_text().unwrap().0;
// deserialize the message into a Transaction struct
let incoming_operation: SignedOp = serde_json::from_str(&msg).unwrap();
let author = general_purpose::STANDARD.encode(&incoming_operation.author());
println!("Received from {:?}", author);
bft_crdt.apply(incoming_operation.clone());
count = count + 1;
interval.tick().await;
}
}
fn every_ten_seconds() -> time::Interval {
time::interval(time::Duration::from_secs(10))
}

54
side-node/tests/crdt.rs Normal file
View File

@@ -0,0 +1,54 @@
use bft_json_crdt::json_crdt::BaseCrdt;
use bft_json_crdt::keypair::make_keypair;
use bft_json_crdt::op::ROOT_ID;
use side_node::bft_crdt::TransactionList;
// case 1 - send valid updates
#[test]
fn test_valid_updates() {
// Insert to bft-crdt.doc on local node, test applying the same operation to a remote node
// and check that the view is the same
let keypair1 = make_keypair();
let mut crdt1 = BaseCrdt::<TransactionList>::new(&keypair1);
let val_a = side_node::utils::fake_generic_transaction_json(String::from("a"));
let val_b = side_node::utils::fake_generic_transaction_json(String::from("b"));
let val_c = side_node::utils::fake_generic_transaction_json(String::from("c"));
let _a = crdt1
.doc
.list
.insert(ROOT_ID, val_a.clone())
.sign(&keypair1);
let _b = crdt1
.doc
.list
.insert(_a.id(), val_b.clone())
.sign(&keypair1);
let _c = crdt1
.doc
.list
.insert(_b.id(), val_c.clone())
.sign(&keypair1);
let keypair2 = make_keypair();
let mut crdt2 = BaseCrdt::<TransactionList>::new(&keypair2);
crdt2.apply(_a.clone());
crdt2.apply(_b);
crdt2.apply(_c.clone());
assert_eq!(
crdt2.doc.list.view(),
crdt1.doc.list.view(),
"views should be equal"
);
crdt2.apply(_a.clone());
crdt2.apply(_a);
assert_eq!(
crdt1.doc.list.view(),
crdt2.doc.list.view(),
"views are still equal after repeated applies"
);
}

View File

@@ -0,0 +1,60 @@
use bft_json_crdt::{
json_crdt::{BaseCrdt, SignedOp},
keypair::make_keypair,
};
use side_node::{
bft_crdt::websocket::Client, bft_crdt::TransactionList, bitcoin, node::SideNode, utils,
};
use tokio::sync::mpsc;
#[tokio::test]
async fn test_distribute_via_websockets() {
let mut node1 = setup("alice").await;
let mut node2 = setup("bob").await;
assert_eq!(node1.current_sha(), node2.current_sha());
let transaction = utils::fake_generic_transaction_json("from_alice".to_string());
let signed_op = node1.add_transaction_local(transaction);
node2.handle_incoming(signed_op);
assert_eq!(node1.current_sha(), node2.current_sha());
let transaction = utils::fake_generic_transaction_json("from_alice2".to_string());
let signed_op = node1.add_transaction_local(transaction);
node2.handle_incoming(signed_op);
assert_eq!(node1.current_sha(), node2.current_sha());
let transaction = utils::fake_generic_transaction_json("from_alice3".to_string());
let signed_op = node1.add_transaction_local(transaction);
node2.handle_incoming(signed_op);
assert_eq!(node1.current_sha(), node2.current_sha());
}
/// Wire everything up, ignoring things we are not using in the test
async fn setup(_: &str) -> SideNode {
// First, load up the keys and create a bft-bft-crdt
let bft_crdt_keys = make_keypair();
let mnemonic_words = bitcoin::keys::make_mnemonic();
let keys = bitcoin::keys::get(mnemonic_words).unwrap();
let bitcoin_wallet = bitcoin::clients::electrum::create_wallet(keys).unwrap();
let crdt = BaseCrdt::<TransactionList>::new(&bft_crdt_keys);
// Channels for internal communication, and a tokio task for stdin input
let (incoming_sender, incoming_receiver) = mpsc::channel::<SignedOp>(32);
let (_, stdin_receiver) = std::sync::mpsc::channel();
// Finally, create the node and return it
let handle = Client::new(incoming_sender).await;
let node = SideNode::new(
crdt,
bft_crdt_keys,
bitcoin_wallet,
incoming_receiver,
stdin_receiver,
handle,
);
node
}

View File

@@ -6,4 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
simple-websockets = "0.1.6"
async-trait = "0.1.52"
ezsockets = { version = "*", features = ["tungstenite"] }
sha256 = "1.5.0"
tokio = { version = "1.17.0", features = ["full"] }
tracing = "0.1.32"
tracing-subscriber = "0.3.9"

View File

@@ -1,34 +1,203 @@
use simple_websockets::{Event, Responder};
use async_trait::async_trait;
use ezsockets::CloseFrame;
use ezsockets::Error;
use ezsockets::Server;
use std::collections::HashMap;
use std::net::SocketAddr;
fn main() {
let event_hub = simple_websockets::launch(8080).expect("failed to listen on port 8080");
let mut clients: HashMap<u64, Responder> = HashMap::new();
println!("Listening for websocket connection...");
loop {
match event_hub.poll_event() {
Event::Connect(client_id, responder) => {
println!("A client connected with id #{}", client_id);
// add their Responder to our `clients` map:
clients.insert(client_id, responder);
}
Event::Disconnect(client_id) => {
println!("Client #{} disconnected.", client_id);
// remove the disconnected client from the clients map:
clients.remove(&client_id);
}
Event::Message(client_id, message) => {
println!("\nReceived a message from client #{}", client_id);
const DEFAULT_ROOM: &str = "main";
let all_clients = clients.keys().collect::<Vec<_>>();
for client in all_clients {
if *client != client_id {
println!("Sending message to client #{}", client);
let responder = clients.get(client).unwrap();
responder.send(message.clone());
}
type SessionID = u8;
type Session = ezsockets::Session<SessionID, ()>;
#[derive(Debug)]
enum Message {
Join {
id: SessionID,
room: String,
},
Send {
from: SessionID,
room: String,
text: String,
},
}
struct ChatServer {
sessions: HashMap<SessionID, Session>,
rooms: HashMap<String, Vec<SessionID>>,
handle: Server<Self>,
}
#[async_trait]
impl ezsockets::ServerExt for ChatServer {
type Call = Message;
type Session = SessionActor;
async fn on_connect(
&mut self,
socket: ezsockets::Socket,
_request: ezsockets::Request,
_address: SocketAddr,
) -> Result<Session, Option<CloseFrame>> {
let id = (0..).find(|i| !self.sessions.contains_key(i)).unwrap_or(0);
let session = Session::create(
|session_handle| SessionActor {
id,
server: self.handle.clone(),
session: session_handle,
room: DEFAULT_ROOM.to_string(),
},
id,
socket,
);
self.sessions.insert(id, session.clone());
self.rooms.get_mut(DEFAULT_ROOM).unwrap().push(id);
Ok(session)
}
async fn on_disconnect(
&mut self,
id: <Self::Session as ezsockets::SessionExt>::ID,
_reason: Result<Option<CloseFrame>, Error>,
) -> Result<(), Error> {
assert!(self.sessions.remove(&id).is_some());
let (ids, n) = self
.rooms
.values_mut()
.find_map(|ids| ids.iter().position(|v| id == *v).map(|n| (ids, n)))
.expect("could not find session in any room");
ids.remove(n);
Ok(())
}
async fn on_call(&mut self, call: Self::Call) -> Result<(), Error> {
match call {
Message::Send { from, room, text } => {
let (ids, sessions): (Vec<SessionID>, Vec<&Session>) = self
.rooms
.get(&room)
.unwrap()
.iter()
.filter(|id| **id != from)
.map(|id| (id, self.sessions.get(id).unwrap()))
.unzip();
tracing::info!(
"sending {hash} to [{sessions}] at `{room}`",
sessions = ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(","),
hash = shappy(text.clone())
);
for session in sessions {
session.text(text.clone()).unwrap();
}
}
}
Message::Join { id, room } => {
let (ids, n) = self
.rooms
.values_mut()
.find_map(|ids| ids.iter().position(|v| id == *v).map(|n| (ids, n)))
.expect("could not find session in any room");
ids.remove(n);
if let Some(ids) = self.rooms.get_mut(&room) {
ids.push(id);
} else {
self.rooms.insert(room.clone(), vec![id]);
}
let sessions = self
.rooms
.get(&room)
.unwrap()
.iter()
.map(|id| self.sessions.get(id).unwrap());
for session in sessions {
session
.text(format!("User with ID: {id} just joined {room} room"))
.unwrap();
}
}
};
Ok(())
}
}
struct SessionActor {
id: SessionID,
server: Server<ChatServer>,
session: Session,
room: String,
}
#[async_trait]
impl ezsockets::SessionExt for SessionActor {
type ID = SessionID;
type Call = ();
fn id(&self) -> &Self::ID {
&self.id
}
async fn on_text(&mut self, text: String) -> Result<(), Error> {
tracing::info!("received: {}", shappy(text.clone()));
if text.starts_with('/') {
let mut args = text.split_whitespace();
let command = args.next().unwrap();
if command == "/join" {
let room = args.next().expect("missing <room> argument").to_string();
tracing::info!("moving {} to {room}", self.id);
self.room = room.clone();
self.server
.call(Message::Join { id: self.id, room })
.unwrap();
} else {
tracing::error!("unrecognized command: {text}");
}
} else {
self.server
.call(Message::Send {
text,
from: self.id,
room: self.room.clone(),
})
.unwrap();
}
Ok(())
}
async fn on_binary(&mut self, bytes: Vec<u8>) -> Result<(), Error> {
// echo bytes back (we use this for a hacky ping/pong protocol for the wasm client demo)
tracing::info!("echoing bytes: {bytes:?}");
self.session.binary("pong".as_bytes())?;
Ok(())
}
async fn on_call(&mut self, call: Self::Call) -> Result<(), Error> {
let () = call;
Ok(())
}
}
fn shappy(text: String) -> String {
let b = text.into_bytes();
sha256::digest(b).to_string()
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let (server, _) = Server::create(|handle| ChatServer {
sessions: HashMap::new(),
rooms: HashMap::from_iter([(DEFAULT_ROOM.to_string(), vec![])]),
handle,
});
ezsockets::tungstenite::run(server, "127.0.0.1:8080")
.await
.unwrap();
}