Skip to content

Commit

Permalink
Port burnchain-neon-controller
Browse files Browse the repository at this point in the history
  • Loading branch information
lgalabru committed May 1, 2020
1 parent aa2d4ff commit 7a45b32
Show file tree
Hide file tree
Showing 33 changed files with 387 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .cargo/config
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[alias]
testnet = "run --package stacks-testnet --"
testnet = "run --package stacks-node --"
bitcoin-neon-controller = "run --package bitcoin-neon-controller --"
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,7 @@ default = ["developer-mode"]
sha2-asm = "0.5.3"

[workspace]
members = [".", "testnet/"]
members = [
".",
"testnet/stacks-node",
"testnet/bitcoin-neon-controller"]
2 changes: 2 additions & 0 deletions testnet/bitcoin-neon-controller/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Dockerfile
target
15 changes: 15 additions & 0 deletions testnet/bitcoin-neon-controller/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "bitcoin-neon-controller"
version = "0.1.0"
authors = ["Ludo Galabru <[email protected]>"]
edition = "2018"

[dependencies]
async-h1 = "1.0.2"
async-std = { version = "1.4.0", features = ["attributes"] }
base64 = "0.12.0"
http-types = "1.0.0"
serde = "1"
serde_derive = "1"
serde_json = { version = "1.0", features = ["arbitrary_precision"] }
toml = "0.5"
24 changes: 24 additions & 0 deletions testnet/bitcoin-neon-controller/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM rust as builder
WORKDIR /src/bitcoin-neon-controller
COPY . .
RUN cd /src/bitcoin-neon-controller && cargo build --target x86_64-unknown-linux-gnu --release
RUN cargo install --target x86_64-unknown-linux-gnu --path /src/bitcoin-neon-controller

FROM alpine
ARG GLIBC_VERSION="2.31-r0"
ARG GLIBC_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk"
ARG GLIBC_BIN_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk"
ARG GLIBC_I18N_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-i18n-${GLIBC_VERSION}.apk"
WORKDIR /
RUN apk --no-cache add --update ca-certificates curl libgcc \
&& curl -L -s -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \
&& curl -L -s -o /tmp/glibc-${GLIBC_VERSION}.apk ${GLIBC_URL} \
&& curl -L -s -o /tmp/glibc-bin-${GLIBC_VERSION}.apk ${GLIBC_BIN_URL} \
&& curl -L -s -o /tmp/glibc-i18n-${GLIBC_VERSION}.apk ${GLIBC_I18N_URL} \
&& apk --no-cache add /tmp/glibc-${GLIBC_VERSION}.apk /tmp/glibc-bin-${GLIBC_VERSION}.apk /tmp/glibc-i18n-${GLIBC_VERSION}.apk \
&& /usr/glibc-compat/sbin/ldconfig /usr/lib /lib \
&& rm /tmp/glibc-${GLIBC_VERSION}.apk /tmp/glibc-bin-${GLIBC_VERSION}.apk /tmp/glibc-i18n-${GLIBC_VERSION}.apk
COPY --from=builder /usr/local/cargo/bin/bitcoin-neon-controller /usr/local/bin/bitcoin-neon-controller
COPY config.toml.default /etc/bitcoin-neon-controller/Config.toml
EXPOSE 3000
CMD ["/usr/local/bin/bitcoin-neon-controller /etc/bitcoin-neon-controller/Config.toml"]
6 changes: 6 additions & 0 deletions testnet/bitcoin-neon-controller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Neon master node

Neon master node is responsible for:
- forwarding authorized RPC calls (such as `sendrawtransaction`, `importpubkey` and `listunspent`) to a centralized bitcoind chain, running in regtest mode.
- mining bitcoin blocks (every 7 secs)
- seed BTC faucet.
7 changes: 7 additions & 0 deletions testnet/bitcoin-neon-controller/config.toml.default
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[neon]
rpc_bind = "0.0.0.0:18443"
block_time = 7000
miner_address = "mx2uds6sgnn9znABQ6iDSSmXY9K5D4SHF9"
bitcoind_rpc_host = "127.0.0.1:18443"
bitcoind_rpc_user = "helium-node"
bitcoind_rpc_pass = "secret"
7 changes: 7 additions & 0 deletions testnet/bitcoin-neon-controller/local-leader.toml.default
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[neon]
rpc_bind = "0.0.0.0:28443"
block_time = 7000
miner_address = "mtFzK54XtpktHj7fKonFExEPEGkUMsiXdy"
bitcoind_rpc_host = "127.0.0.1:18443"
bitcoind_rpc_user = "helium-node"
bitcoind_rpc_pass = "secret"
287 changes: 287 additions & 0 deletions testnet/bitcoin-neon-controller/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
#[macro_use] extern crate serde_derive;

use std::io::{BufReader, Read};
use std::fs::File;
use std::env;
use std::thread;
use std::time::Duration;

use async_h1::{client};
use async_std::net::{TcpListener, TcpStream};
use async_std::prelude::*;
use async_std::task;
use base64::{encode};
use http_types::{
Response,
StatusCode,
Method,
headers,
Request,
Url
};
use serde::Deserialize;
use serde_json::Deserializer;
use toml;

#[async_std::main]
async fn main() -> http_types::Result<()> {
let argv : Vec<String> = env::args().collect();

// Guard: config missing
if argv.len() != 2 {
panic!("Config argument missing");
}

let config = ConfigFile::from_path(&argv[1]);

if is_bootstrap_chain_required(&config).await? {
println!("Bootstrapping chain");
generate_blocks(200, &config).await;
}

// Start a loop in a separate thread, generating new blocks
// on a given frequence (coming from config).
let conf = config.clone();
thread::spawn(move || {
let block_time = Duration::from_millis(conf.neon.block_time);

loop {
println!("Generating block");
async_std::task::block_on(async {
generate_blocks(1, &conf).await;
});
thread::sleep(block_time);
}
});

// Open up a TCP connection and create a URL.
let bind_addr = config.neon.rpc_bind.clone();
let listener = TcpListener::bind(bind_addr).await?;
let addr = format!("http://{}", listener.local_addr()?);
println!("Listening on {}", addr);

// For each incoming TCP connection, spawn a task and call `accept`.
let mut incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let stream = stream?;
let addr = addr.clone();
let config = config.clone();

task::spawn(async move {
if let Err(err) = accept(addr, stream, &config).await {
eprintln!("{}", err);
}
});
}
Ok(())
}

// Take a TCP stream, and convert it into sequential HTTP request / response pairs.
async fn accept(addr: String, stream: TcpStream, config: &ConfigFile) -> http_types::Result<()> {
println!("starting new connection from {}", stream.peer_addr()?);
async_h1::accept(&addr, stream.clone(), |mut req| async {
match (req.method(), req.url().path(), req.header(&headers::CONTENT_TYPE)) {
(Method::Get, "/ping", Some(_content_type)) => {
Ok(Response::new(StatusCode::Ok))
},
(Method::Post, "/", Some(_content_types)) => {

let (res, buffer) = async_std::task::block_on(async move {
let mut buffer = Vec::new();
let mut body = req.take_body();
let res = body.read_to_end(&mut buffer).await;
(res, buffer)
});

// Guard: can't be read
if res.is_err() {
return Ok(Response::new(StatusCode::MethodNotAllowed))
}

let mut deserializer = Deserializer::from_slice(&buffer);

// Guard: can't be parsed
let rpc_req: RPCRequest = match RPCRequest::deserialize(&mut deserializer) {
Ok(rpc_req) => rpc_req,
_ => return Ok(Response::new(StatusCode::MethodNotAllowed))
};

println!("{:?}", rpc_req);

let authorized_methods = vec![
"listunspent",
"importaddress",
"sendrawtransaction"];

// Guard: unauthorized method
if !authorized_methods.contains(&rpc_req.method.as_str()) {
return Ok(Response::new(StatusCode::MethodNotAllowed))
}

// Forward the request
let stream = TcpStream::connect(config.neon.bitcoind_rpc_host.clone()).await?;
let body = serde_json::to_vec(&rpc_req).unwrap();
let req = build_request(&config, body);
client::connect(stream.clone(), req).await
},
_ => {
Ok(Response::new(StatusCode::MethodNotAllowed))
}
}
}).await?;

Ok(())
}

async fn is_bootstrap_chain_required(config: &ConfigFile) -> http_types::Result<bool> {

let req = RPCRequest::is_chain_bootstrapped();
let stream = TcpStream::connect(config.neon.bitcoind_rpc_host.clone()).await?;
let body = serde_json::to_vec(&req).unwrap();
let mut resp = client::connect(stream.clone(), build_request(&config, body)).await?;

let (res, buffer) = async_std::task::block_on(async move {
let mut buffer = Vec::new();
let mut body = resp.take_body();
let res = body.read_to_end(&mut buffer).await;
(res, buffer)
});

// Guard: can't be read
if res.is_err() {
panic!("Chain height could not be determined")
}
// let mut deserializer = Deserializer::from_slice(&buffer);

let mut deserializer = Deserializer::from_slice(&buffer);

// Guard: can't be parsed
let rpc_resp: RPCResult = match RPCResult::deserialize(&mut deserializer) {
Ok(rpc_req) => rpc_req,
_ => panic!("Chain height could not be determined")
};

match (rpc_resp.result, rpc_resp.error) {
(Some(_), None) => return Ok(false),
(None, Some(error)) => {
if let Some(keys) = error.as_object() {
if let Some(message) = keys.get("message") {
if let Some(message) = message.as_str() {
if message == "Block height out of range" {
return Ok(true)
}
}
}
}
}
(_, _) => {}
}

panic!("Chain height could not be determined")
}

async fn generate_blocks(blocks_count: u64, config: &ConfigFile) {
let rpc_addr = config.neon.bitcoind_rpc_host.clone();
let miner_address = config.neon.miner_address.clone();

let rpc_req = RPCRequest::generate_next_block_req(blocks_count, miner_address.clone());

let stream = TcpStream::connect(rpc_addr).await.unwrap();
let body = serde_json::to_vec(&rpc_req).unwrap();
let req = build_request(&config, body);
client::connect(stream.clone(), req).await.unwrap();
}

fn build_request(config: &ConfigFile, body: Vec<u8>) -> Request {
let url = Url::parse(&format!("http://{}/", config.neon.bitcoind_rpc_host)).unwrap();
let mut req = Request::new(Method::Post, url);
req.append_header("Authorization", config.neon.authorization_token()).unwrap();
req.append_header("Content-Type", "application/json").unwrap();
req.append_header("Content-Length", format!("{}", body.len())).unwrap();
req.append_header("Host", format!("{}", config.neon.bitcoind_rpc_host)).unwrap();
req.set_body(body);
req
}

#[derive(Debug, Clone, Deserialize, Serialize)]
/// JSONRPC Request
pub struct RPCRequest {
/// The name of the RPC call
pub method: String,
/// Parameters to the RPC call
pub params: Vec<serde_json::Value>,
/// Identifier for this Request, which should appear in the response
pub id: serde_json::Value,
/// jsonrpc field, MUST be "2.0"
pub jsonrpc: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RPCResult {
/// The error returned by the RPC call
pub error: Option<serde_json::Value>,
/// The value returned by the RPC call
pub result: Option<serde_json::Value>,
}

impl RPCRequest {

pub fn generate_next_block_req(blocks_count: u64, address: String) -> RPCRequest {
RPCRequest {
method: "generatetoaddress".to_string(),
params: vec![blocks_count.into(), address.into()],
id: 0.into(),
jsonrpc: Some("2.0".to_string())
}
}

pub fn is_chain_bootstrapped() -> RPCRequest {
RPCRequest {
method: "getblockhash".to_string(),
params: vec![200.into()],
id: 0.into(),
jsonrpc: Some("2.0".to_string())
}
}
}

#[derive(Debug, Clone, Deserialize)]
pub struct ConfigFile {
/// Regtest node
neon: RegtestConfig,
}

impl ConfigFile {

pub fn from_path(path: &str) -> ConfigFile {
let path = File::open(path).unwrap();
let mut config_reader = BufReader::new(path);
let mut config = vec![];
config_reader.read_to_end(&mut config).unwrap();
toml::from_slice(&config[..]).unwrap()
}
}

#[derive(Debug, Clone, Deserialize)]
pub struct RegtestConfig {
/// Proxy's port
rpc_bind: String,
/// Duration between blocks
block_time: u64,
/// Address receiving coinbases and mining fee
miner_address: String,
/// RPC address used by bitcoind
bitcoind_rpc_host: String,
/// Credential - username
bitcoind_rpc_user: String,
/// Credential - password
bitcoind_rpc_pass: String,
}

impl RegtestConfig {

pub fn authorization_token(&self) -> String {
let token = encode(format!("{}:{}", self.bitcoind_rpc_user, self.bitcoind_rpc_pass));
format!("Basic {}", token)
}
}
7 changes: 6 additions & 1 deletion testnet/Cargo.toml → testnet/stacks-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ secp256k1 = { version = "0.11.5" }
serde = "1"
serde_derive = "1"
serde_json = { version = "1.0", features = ["arbitrary_precision"] }
stacks = { package = "blockstack-core", path = "../." }
stacks = { package = "blockstack-core", path = "../../." }
toml = "0.5.6"
prometheus = { version = "0.8", optional = true }

[[bin]]
name = "stacks-node"
path = "src/main.rs"

[features]
default = []
prometheus_monitoring = ["prometheus"]
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 7a45b32

Please sign in to comment.