Skip to content

Commit

Permalink
Fix bitcoin lockdrop audit issues (#194)
Browse files Browse the repository at this point in the history
* Fix all tests

* Fix lockdrop script compiler, using bitcoin library
* Added tests for btc_utils

* Style fixes
  • Loading branch information
akru authored Jul 3, 2020
1 parent c7f886c commit 9be1825
Show file tree
Hide file tree
Showing 23 changed files with 392 additions and 665 deletions.
302 changes: 185 additions & 117 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions bin/node/cli/lockdrop-oracle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ reqwest = { version = "0.10", default-features = false, features = ["rustls-tls"
web3 = { version = "0.11", default-features = false, features = ["http","tls"] }
sp-core = "2.0.0-rc4"
sc-service = "0.8.0-rc4"
bitcoin-script = "0.1.1"
bitcoin = "0.23.0"
libsecp256k1 = "0.3.5"
serde_json = "1.0"
ripemd160 = "0.8.0"
structopt = "0.3.8"
bs58 = "0.3"
log = "0.4.8"
hex = "0.4"
tide = "0.8"

[dev-dependencies]
hex-literal = "0.2.1"
93 changes: 47 additions & 46 deletions bin/node/cli/lockdrop-oracle/src/btc_utils.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,58 @@
use codec::Encode;
use ripemd160::{Digest, Ripemd160};
use sp_core::{ecdsa, hashing::sha2_256};
//! Bitcoin locking helpers.
pub const BTC_TESTNET: u8 = 0x6f;
//pub const BTC_MAINNET: u8 = 0x00;
use bitcoin::blockdata::script::Script;
use bitcoin::network::constants::Network;
use bitcoin::util::address::Address;
use bitcoin::util::key::PublicKey;
use bitcoin_script::bitcoin_script;
use sp_core::ecdsa;

/// Bitcoin RIPEMD160 hashing function.
pub fn ripemd160(data: &[u8]) -> [u8; 20] {
let mut hasher = Ripemd160::new();
hasher.input(data);
let mut output = [0u8; 20];
output.copy_from_slice(&hasher.result());
output
/// Encode block delay in BIP68 standard
fn bip68_encode(blocks: u32) -> u32 {
0x0000ffff & blocks
}

/// Compile BTC sequence lock script for givent public key and duration in blocks.
pub fn lock_script(public: &ecdsa::Public, duration: u64) -> Vec<u8> {
let blocks = duration / 600;
let full_public = secp256k1::PublicKey::parse_slice(public.as_ref(), None)
.expect("public key has correct length")
.serialize();
blocks.using_encoded(|enc_blocks| {
let mut output = vec![];
output.extend(vec![enc_blocks.len() as u8]); // Lock duration length
output.extend(enc_blocks); // Lock duration in blocks
output.extend(vec![0x27, 0x55]); // OP_CHECKSEQUENCEVERIFY OP_DROP
output.extend(vec![full_public.len() as u8 - 1]); // Public key lenght
output.extend(&full_public[1..]); // Public key
output.extend(vec![0xAC]); // OP_CHECKSIG
output
})
pub fn lock_script(public: &ecdsa::Public, duration: u32) -> Script {
let public_key = PublicKey::from_slice(public.as_ref()).unwrap();
let blocks = bip68_encode(duration) as i64;
let script = bitcoin_script! {
<blocks>
OP_CSV
OP_DROP
<public_key>
OP_CHECKSIG
};
script.to_p2sh()
}

/// Get hash of binary BTC script.
pub fn script_hash(script: &[u8]) -> [u8; 20] {
ripemd160(&sha2_256(script)[..])
pub fn to_address(public: &ecdsa::Public) -> String {
let public_key = PublicKey::from_slice(public.as_ref()).unwrap();
Address::p2pkh(&public_key, Network::Testnet).to_string()
}

/// Compile BTC pay-to-script-hash script for given script hash.
pub fn p2sh(script_hash: &[u8; 20]) -> Vec<u8> {
let mut output = vec![];
output.extend(vec![0xa9, 0x14]); // OP_HASH160 20
output.extend(script_hash); // <scriptHash>
output.extend(vec![0x87]); // OP_EQUAL
output
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
use sp_core::crypto::Public;

#[test]
fn test_lock_scipt() {
let public = ecdsa::Public::from_slice(
&hex!["038ea27103fb646a2cea9eca9080737e0b23640caaaef2853416c9b286b353313e"][..],
);
let duration = 10;
let script = lock_script(&public, duration);
let address = Address::from_script(&script, Network::Testnet).unwrap();
assert_eq!(address.to_string(), "2MuJcWGWe8XkPc6h7pt6vQDyaTwDZxKJZ8p");
}

/// Get Bitcoin addres for given ECDSA public key and network tag.
/// Note: It works for `1`-prefixed addresses
pub fn to_address(public_key: &ecdsa::Public, network: u8) -> String {
let mut key_hash = vec![network];
key_hash.extend(&ripemd160(&sha2_256(public_key.as_ref())[..])[..]);
let check_sum = sha2_256(&sha2_256(&key_hash)[..]);
key_hash.extend(&check_sum[0..4]);
bs58::encode(key_hash).into_string()
#[test]
fn test_to_address() {
let public = ecdsa::Public::from_full(
&hex!["0431e12c2db27f3b07fcc560cdbff90923bf9b5b03769103a44b38426f9469172f3eef59e4f01df729428161c33ec5b32763e2e5a0072551b7808ae9d89286b37b"][..]
).unwrap();
assert_eq!(to_address(&public), "mzUQaN6vnYDYNNYJVpRz2ipxLcWsQg6b8z");
}
}
2 changes: 1 addition & 1 deletion bin/node/cli/lockdrop-oracle/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Lockdrop module CLI parameters.
//! Lockdrop module CLI parameters.
use structopt::StructOpt;

Expand Down
2 changes: 2 additions & 0 deletions bin/node/cli/lockdrop-oracle/src/eth_utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Ethereum locking helpers.
use sp_core::{keccak_256, H160, U256};

/// Get Ethereum address for given ECDSA public key.
Expand Down
35 changes: 17 additions & 18 deletions bin/node/cli/lockdrop-oracle/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! Web-server helper for Lockdrop runtime module.
#![feature(proc_macro_hygiene)]

use codec::Decode;
use pallet_plasm_lockdrop::LockCheck;
use tide::{http::StatusCode, Response};
use web3::futures::Future;

mod btc_utils;
mod eth_utils;
mod cli;
mod eth_utils;

pub use cli::Config;

Expand Down Expand Up @@ -36,7 +38,11 @@ pub async fn start(config: Config) {
"BTC lock check request: {:#?}", lock
);

let uri = format!("{}/{}", req.state().bitcoin_endpoint, hex::encode(lock.tx_hash));
let uri = format!(
"{}/{}",
req.state().bitcoin_endpoint,
hex::encode(lock.tx_hash)
);
let tx: serde_json::Value = reqwest::blocking::get(uri.as_str())?.json()?;
log::debug!(
target: "lockdrop-oracle",
Expand All @@ -60,7 +66,7 @@ pub async fn start(config: Config) {
return Ok(Response::new(StatusCode::BadRequest));
}

let lock_sender = btc_utils::to_address(&lock.public_key, btc_utils::BTC_TESTNET);
let lock_sender = btc_utils::to_address(&lock.public_key);
log::debug!(
target: "lockdrop-oracle",
"BTC address for public key {}: {}",
Expand All @@ -74,23 +80,17 @@ pub async fn start(config: Config) {
}

// assembly bitcoin script for given params
let lock_script = btc_utils::lock_script(&lock.public_key, lock.duration);
let blocks = (lock.duration / 600) as u32;
let lock_script = btc_utils::lock_script(&lock.public_key, blocks);
log::debug!(
target: "lockdrop-oracle",
"BTC lock script for public key ({}) and duration ({}): {}",
lock.public_key,
"Lock script address for public ({}), duration({}): {}",
hex::encode(lock.public_key),
lock.duration,
hex::encode(lock_script.clone()),
);
let script = btc_utils::p2sh(&btc_utils::script_hash(&lock_script[..]));
log::debug!(
target: "lockdrop-oracle",
"P2SH for script {}: {}",
hex::encode(lock_script),
hex::encode(&script),
hex::encode(lock_script.as_bytes()),
);
// check script code
if tx_script != script {
if tx_script != lock_script.into_bytes() {
log::debug!(target: "lockdrop-oracle", "lock script mismatch");
return Ok(Response::new(StatusCode::BadRequest));
}
Expand All @@ -107,9 +107,8 @@ pub async fn start(config: Config) {
"ETH lock check request: {:#?}", lock
);

let (_eloop, transport) = web3::transports::Http::new(
req.state().ethereum_endpoint.as_str()
).unwrap();
let (_eloop, transport) =
web3::transports::Http::new(req.state().ethereum_endpoint.as_str()).unwrap();
let web3 = web3::Web3::new(transport);
let block_number = web3.eth().block_number().wait()?;
let tx = web3
Expand Down
4 changes: 3 additions & 1 deletion bin/node/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ pub fn run() -> sc_cli::Result<()> {
Some(Subcommand::LockdropOracle(config)) => {
sc_cli::init_logger("");
log::info!("Plasm Lockdrop oracle launched.");
Ok(futures::executor::block_on(lockdrop_oracle::start(config.clone())))
Ok(futures::executor::block_on(lockdrop_oracle::start(
config.clone(),
)))
}
}
}
2 changes: 0 additions & 2 deletions bin/node/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ mod browser;
mod cli;
#[cfg(feature = "cli")]
mod command;
#[cfg(feature = "oracle")]
mod oracle;

#[cfg(feature = "browser")]
pub use browser::*;
Expand Down
96 changes: 47 additions & 49 deletions bin/node/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@

use std::sync::Arc;

use plasm_primitives::{Block, BlockNumber, AccountId, Index, Balance, Hash};
use plasm_primitives::{AccountId, Balance, Block, BlockNumber, Hash, Index};
use plasm_runtime::UncheckedExtrinsic;
use sp_api::ProvideRuntimeApi;
use sp_transaction_pool::TransactionPool;
use sp_blockchain::{Error as BlockChainError, HeaderMetadata, HeaderBackend};
use sp_consensus::SelectChain;
use sc_keystore::KeyStorePtr;
use sp_consensus_babe::BabeApi;
use sc_consensus_epochs::SharedEpochChanges;
use sc_consensus_babe::{Config, Epoch};
use sc_consensus_babe_rpc::BabeRpcHandler;
use sc_finality_grandpa::{SharedVoterState, SharedAuthoritySet};
use sc_consensus_epochs::SharedEpochChanges;
use sc_finality_grandpa::{SharedAuthoritySet, SharedVoterState};
use sc_finality_grandpa_rpc::GrandpaRpcHandler;
use sc_keystore::KeyStorePtr;
use sc_rpc_api::DenyUnsafe;
use sp_api::ProvideRuntimeApi;
use sp_block_builder::BlockBuilder;
use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
use sp_consensus::SelectChain;
use sp_consensus_babe::BabeApi;
use sp_transaction_pool::TransactionPool;

/// Light client extra dependencies.
pub struct LightDeps<C, F, P> {
Expand Down Expand Up @@ -78,24 +78,27 @@ pub struct FullDeps<C, P, SC> {
}

/// Instantiate all Full RPC extensions.
pub fn create_full<C, P, M, SC>(
deps: FullDeps<C, P, SC>,
) -> jsonrpc_core::IoHandler<M> where
pub fn create_full<C, P, M, SC>(deps: FullDeps<C, P, SC>) -> jsonrpc_core::IoHandler<M>
where
C: ProvideRuntimeApi<Block>,
C: HeaderBackend<Block> + HeaderMetadata<Block, Error=BlockChainError> + 'static,
C: HeaderBackend<Block> + HeaderMetadata<Block, Error = BlockChainError> + 'static,
C: Send + Sync + 'static,
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, AccountId, Index>,
C::Api: pallet_contracts_rpc::ContractsRuntimeApi<Block, AccountId, Balance, BlockNumber>,
C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance, UncheckedExtrinsic>,
C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<
Block,
Balance,
UncheckedExtrinsic,
>,
C::Api: BabeApi<Block>,
C::Api: BlockBuilder<Block>,
P: TransactionPool + 'static,
M: jsonrpc_core::Metadata + Default,
SC: SelectChain<Block> +'static,
SC: SelectChain<Block> + 'static,
{
use substrate_frame_rpc_system::{FullSystem, SystemApi};
use pallet_contracts_rpc::{Contracts, ContractsApi};
use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApi};
use substrate_frame_rpc_system::{FullSystem, SystemApi};

let mut io = jsonrpc_core::IoHandler::default();
let FullDeps {
Expand All @@ -116,43 +119,38 @@ pub fn create_full<C, P, M, SC>(
shared_authority_set,
} = grandpa;

io.extend_with(
SystemApi::to_delegate(FullSystem::new(client.clone(), pool, deny_unsafe))
);
io.extend_with(SystemApi::to_delegate(FullSystem::new(
client.clone(),
pool,
deny_unsafe,
)));
// Making synchronous calls in light client freezes the browser currently,
// more context: https://github.com/paritytech/substrate/pull/3480
// These RPCs should use an asynchronous caller instead.
io.extend_with(
ContractsApi::to_delegate(Contracts::new(client.clone()))
);
io.extend_with(
TransactionPaymentApi::to_delegate(TransactionPayment::new(client.clone()))
);
io.extend_with(
sc_consensus_babe_rpc::BabeApi::to_delegate(
BabeRpcHandler::new(
client,
shared_epoch_changes,
keystore,
babe_config,
select_chain,
deny_unsafe,
),
)
);
io.extend_with(
sc_finality_grandpa_rpc::GrandpaApi::to_delegate(
GrandpaRpcHandler::new(shared_authority_set, shared_voter_state)
)
);
io.extend_with(ContractsApi::to_delegate(Contracts::new(client.clone())));
io.extend_with(TransactionPaymentApi::to_delegate(TransactionPayment::new(
client.clone(),
)));
io.extend_with(sc_consensus_babe_rpc::BabeApi::to_delegate(
BabeRpcHandler::new(
client,
shared_epoch_changes,
keystore,
babe_config,
select_chain,
deny_unsafe,
),
));
io.extend_with(sc_finality_grandpa_rpc::GrandpaApi::to_delegate(
GrandpaRpcHandler::new(shared_authority_set, shared_voter_state),
));

io
}

/// Instantiate all Light RPC extensions.
pub fn create_light<C, P, M, F>(
deps: LightDeps<C, F, P>,
) -> jsonrpc_core::IoHandler<M> where
pub fn create_light<C, P, M, F>(deps: LightDeps<C, F, P>) -> jsonrpc_core::IoHandler<M>
where
C: sp_blockchain::HeaderBackend<Block>,
C: Send + Sync + 'static,
F: sc_client_api::light::Fetcher<Block> + 'static,
Expand All @@ -165,12 +163,12 @@ pub fn create_light<C, P, M, F>(
client,
pool,
remote_blockchain,
fetcher
fetcher,
} = deps;
let mut io = jsonrpc_core::IoHandler::default();
io.extend_with(
SystemApi::<Hash, AccountId, Index>::to_delegate(LightSystem::new(client, remote_blockchain, fetcher, pool))
);
io.extend_with(SystemApi::<Hash, AccountId, Index>::to_delegate(
LightSystem::new(client, remote_blockchain, fetcher, pool),
));

io
}
Loading

0 comments on commit 9be1825

Please sign in to comment.