diff --git a/Cargo.lock b/Cargo.lock index e7121c4cde..e7d2011d8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,16 @@ dependencies = [ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "btc_spv_bin" +version = "0.1.0" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "build_const" version = "0.2.1" @@ -3127,6 +3137,34 @@ dependencies = [ "solana-sdk 0.19.0-pre0", ] +[[package]] +name = "solana-bitcoin-spv-api" +version = "0.0.1" +dependencies = [ + "bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-sdk 0.19.0-pre0", +] + +[[package]] +name = "solana-bitcoin-spv-program" +version = "0.0.1" +dependencies = [ + "bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-sdk 0.19.0-pre0", +] + [[package]] name = "solana-bpf-loader-api" version = "0.19.0-pre0" diff --git a/Cargo.toml b/Cargo.toml index 305ad582a2..66dacf5d1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ members = [ "programs/bpf_loader_program", "programs/budget_api", "programs/budget_program", + "programs/btc_spv_program", + "programs/btc_spv_api", + "programs/btc_spv_bin", "programs/config_api", "programs/config_program", "programs/config_tests", diff --git a/programs/btc_spv_api/.gitignore b/programs/btc_spv_api/.gitignore new file mode 100644 index 0000000000..5404b132db --- /dev/null +++ b/programs/btc_spv_api/.gitignore @@ -0,0 +1,2 @@ +/target/ +/farf/ diff --git a/programs/btc_spv_api/Cargo.toml b/programs/btc_spv_api/Cargo.toml new file mode 100644 index 0000000000..d0f8512c3c --- /dev/null +++ b/programs/btc_spv_api/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "solana-btc-spv-api" +version = "0.19.0-pre0" +description = "Solana Bitcoin spv parsing program api" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +bincode = "1.1.4" +chrono = { version = "0.4.0", features = ["serde"] } +log = "0.4.2" +num-derive = "0.2" +num-traits = "0.2" +serde = "1.0.99" +serde_derive = "1.0.98" +solana-sdk = { path = "../../sdk", version = "0.19.0-pre0"} + +[lib] +crate-type = ["lib"] +name = "solana_btc_spv_api" diff --git a/programs/btc_spv_api/README.md b/programs/btc_spv_api/README.md new file mode 100644 index 0000000000..2fded58fcb --- /dev/null +++ b/programs/btc_spv_api/README.md @@ -0,0 +1,64 @@ +## Problem + +Inter-chain applications are not new to the digital asset ecosystem; in fact, even +the smaller centralized exchanges still categorically dwarf all single chain dapps +put together in terms of users and volume. They command massive valuations and +have spent years effectively optimizing their core products for a broad range of +end users. However, their basic operations center around mechanisms that require +their users to unilaterally trust them, typically with little to no recourse +or protection from accidental loss. This has led to the broader digital asset +ecosystem being fractured along network lines because interoperability solutions typically: + * Are technically complex to fully implement + * Create unstable network scale incentive structures + * Require consistent and high level cooperation between stakeholders + + +## Proposed Solution + +Simple Payment Verification (SPV) is a generic term for a range of different +methodologies used by light clients on most major blockchain networks to verify +aspects of the network state without the burden of fully storing and maintaining +the chain itself. In most cases, this means relying on a form of hash tree to +supply a proof of the presence of a given transaction in a certain block by +comparing against a root hash in that block’s header or equivalent. This allows +a light client or wallet to reach a probabilistic level of certainty about +on-chain events by itself with a minimum of trust required with regard to network nodes. + +Traditionally the process of assembling and validating these proofs is carried +out off chain by nodes, wallets, or other clients, but it also offers a potential +mechanism for inter-chain state verification. However, by moving the capability +to validate SPV proofs on-chain as a smart contract while leveraging the archival +properties inherent to the blockchain, it is possible to construct a system for +programmatically detecting and verifying transactions on other networks without +the involvement of any type of trusted oracle or complex multi-stage consensus +mechanism. This concept is broadly generalisable to any network with an SPV +mechanism and can even be operated bilaterally on other smart contract platforms, +opening up the possibility of cheap, fast, inter-chain transfer of value without +relying on collateral, hashlocks, or trusted intermediaries. + +Opting to take advantage of well established and developmentally stable mechanisms +already common to all major blockchains allows SPV based interoperability solutions +to be dramatically simpler than orchestrated multi-stage approaches. As part of +this, they dispense with the need for widely agreed upon cross chain communication +standards and the large multi-party organizations that write them in favor of a +set of discrete contract-based services that can be easily utilized by caller +contracts through a common abstraction format. This will set the groundwork for +a broad range of dapps and contracts able to interoperate across the variegated +and every growing platform ecosystem. + +## Terminology + +SPV Program - Client-facing interface for the inter-chain SPV system, manages participant roles. +SPV Engine - Validates transaction proofs, subset of the SPV Program. +Client - The caller to the SPV Program, typically another solana contract. +Prover - Party who generates proofs for transactions and submits them to the SPV Program. +Transaction Proof - Created by Provers, contains a merkle proof, transaction, and blockheader reference. +Merkle Proof - Basic SPV proof that validates the presence of a transaction in a certain block. +Block Header - Represents the basic parameters and relative position of a given block. +Proof Request - An order placed by a client for verification of transaction(s) by provers. +Header Store - A data structure for storing and referencing ranges of block headers in proofs. +Client Request - Transaction from the client to the SPV Program to trigger creation of a Proof Request. +Sub-account - A Solana account owned by another contract account, without its own private key. + +For more information on the Inter-chain SPV system, see the book section at: +https://solana-labs.github.io/book/interchain-transaction-verification.html diff --git a/programs/btc_spv_api/src/header_store.rs b/programs/btc_spv_api/src/header_store.rs new file mode 100644 index 0000000000..c4fa974b73 --- /dev/null +++ b/programs/btc_spv_api/src/header_store.rs @@ -0,0 +1,102 @@ +#[allow(unused_imports)] +use crate::spv_state::*; +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +// HeaderStore is a data structure that allows linked list style cheap appends and +// sequential reads, but also has a "lookup index" to speed up random access +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum HeaderStoreError { + InvalidHeader, + GroupExists, + GroupDNE, + InvalidBlockHeight, +} + +// AccountList is a linked list of groups of blockheaders. It stores sequential blockheaders +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct HeaderStore { + pub index: Vec, + // number of header entries to include per group account + pub group_size: u16, + // base_height is the height of the first header in the first headerAccount + pub base_height: u32, + // top_height is the running last header loaded + pub top_height: u32, + // account that administrates the headerstore and benefits from fees accrued + pub owner: Pubkey, +} + +impl HeaderStore { + pub fn get_group(self, block_height: u32) -> Result { + if block_height < self.base_height || block_height > self.top_height { + Err(HeaderStoreError::InvalidBlockHeight) + } else { + let gheight = (block_height - self.base_height) / u32::from(self.group_size); + let grouppk: Pubkey = self.index[gheight as usize]; + Ok(grouppk) + } + } + + pub fn top_group(self) -> Result { + if self.index.is_empty() { + Err(HeaderStoreError::GroupDNE) + } else { + let grouppk: Pubkey = *self.index.last().unwrap(); + Ok(grouppk) + } + } + + pub fn append_header(mut self, blockheader: &BlockHeader) -> Result<(), HeaderStoreError> { + match self.top_group() { + Ok(n) => { + let group = n; + Ok(()) + } + Err(e) => { + // HeaderStore is empty need to create first group + if HeaderStoreError::GroupDNE == e { + Ok(()) + } else { + Err(e) + } + //reinsert + } + } + } + + pub fn replace_header( + mut self, + blockheader: &BlockHeader, + block_height: u32, + ) -> Result<(), HeaderStoreError> { + match self.get_group(block_height) { + Err(e) => Err(HeaderStoreError::InvalidHeader), + Ok(n) => { + let group = n; + Ok(()) + } + } + //reinsert + } + + pub fn append_group(mut self, pubkey: Pubkey) -> Result<(), HeaderStoreError> { + if self.index.contains(&pubkey) { + // group to be appended is already in the index + Err(HeaderStoreError::GroupExists) + } else { + Ok(()) + //reinsert + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct HeaderAccountInfo { + // parent stores the pubkey of the parent AccountList + pub parent: Pubkey, + // stores a vec of BlockHeader structs + pub headers: Vec, + // next DataAccount in the chain or none if last + pub next: Option, +} diff --git a/programs/btc_spv_api/src/lib.rs b/programs/btc_spv_api/src/lib.rs new file mode 100644 index 0000000000..4385c2187f --- /dev/null +++ b/programs/btc_spv_api/src/lib.rs @@ -0,0 +1,20 @@ +#![allow(unused_imports)] +#![allow(unused_variables)] +#![allow(unused_mut)] +#![allow(dead_code)] +// This version is a work in progress and contains placeholders and incomplete components +pub mod header_store; +pub mod spv_instruction; +pub mod spv_processor; +pub mod spv_state; +pub mod utils; + +pub const BTC_SPV_PROGRAM_ID: [u8; 32] = [ + 2, 202, 42, 59, 228, 51, 182, 147, 162, 245, 234, 78, 205, 37, 131, 154, 110, 252, 154, 254, + 190, 13, 90, 231, 198, 144, 239, 96, 0, 0, 0, 0, +]; + +solana_sdk::solana_name_id!( + BTC_SPV_PROGRAM_ID, + "BtcSpv1111111111111111111111111111111111111" +); diff --git a/programs/btc_spv_api/src/spv_instruction.rs b/programs/btc_spv_api/src/spv_instruction.rs new file mode 100644 index 0000000000..64740230bf --- /dev/null +++ b/programs/btc_spv_api/src/spv_instruction.rs @@ -0,0 +1,78 @@ +//! Spv proof Verification Program +use crate::id; +use crate::spv_state::*; +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::instruction::{AccountMeta, Instruction}; +use solana_sdk::pubkey::Pubkey; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum SpvInstruction { + // Client Places request for a matching proof + // key 0 - Signer + // key 1 - Account in which to record the Request and proof + ClientRequest(ClientRequestInfo), + + // Used by clients to cancel a pending proof request + // key 0 - signer + // key 1 - Request to cancel + CancelRequest, + + // used to submit a proof matching a posted BitcoinTxHash or for own benefit + // key 0 - signer + // key 1 - Request to prove + SubmitProof(Proof), +} + +pub fn client_request( + owner: &Pubkey, + txhash: BitcoinTxHash, + fee: u64, + confirmations: u8, + difficulty: u64, + expiration: Option, +) -> Instruction { + let account_meta = vec![AccountMeta::new(*owner, true)]; + Instruction::new( + id(), + &SpvInstruction::ClientRequest(ClientRequestInfo { + txhash, + confirmations, + fee, + difficulty, + expiration, + }), + account_meta, + ) +} + +pub fn cancel_request(owner: &Pubkey, request: &Pubkey) -> Instruction { + let account_meta = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*request, false), + ]; + Instruction::new(id(), &SpvInstruction::CancelRequest, account_meta) +} + +pub fn submit_proof( + submitter: &Pubkey, + proof: MerkleProof, + headers: HeaderChain, + transaction: Transaction, + request: &Pubkey, +) -> Instruction { + let account_meta = vec![ + AccountMeta::new(*submitter, true), + AccountMeta::new(*request, false), + ]; + Instruction::new( + id(), + &SpvInstruction::SubmitProof(Proof { + submitter: *submitter, + proof, + headers, + transaction, + request: *request, + }), + account_meta, + ) +} diff --git a/programs/btc_spv_api/src/spv_processor.rs b/programs/btc_spv_api/src/spv_processor.rs new file mode 100644 index 0000000000..257f115ce2 --- /dev/null +++ b/programs/btc_spv_api/src/spv_processor.rs @@ -0,0 +1,166 @@ +//! Bitcoin SPV proof verifier program +//! Receive merkle proofs and block headers, validate transaction +use crate::spv_instruction::*; +use crate::spv_state::*; +#[allow(unused_imports)] +use crate::utils::decode_hex; +use log::*; +use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; +use solana_sdk::pubkey::Pubkey; + +pub struct SpvProcessor {} + +impl SpvProcessor { + pub fn validate_header_chain( + headers: HeaderChain, + proof_req: &ProofRequest, + ) -> Result<(), InstructionError> { + // disabled for time being + //not done yet, needs difficulty average/variance checking still + Ok(()) + } + + #[allow(clippy::needless_pass_by_value)] + fn map_to_invalid_arg(err: std::boxed::Box) -> InstructionError { + warn!("Deserialize failed, not a valid state: {:?}", err); + InstructionError::InvalidArgument + } + + fn deserialize_proof(data: &[u8]) -> Result { + let proof_state: AccountState = + bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + if let AccountState::Verification(proof) = proof_state { + Ok(proof) + } else { + error!("Not a valid proof"); + Err(InstructionError::InvalidAccountData) + } + } + + fn deserialize_request(data: &[u8]) -> Result { + let req_state: AccountState = + bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + if let AccountState::Request(info) = req_state { + Ok(info) + } else { + error!("Not a valid proof request"); + Err(InstructionError::InvalidAccountData) + } + } + + pub fn check_account_unallocated(data: &[u8]) -> Result<(), InstructionError> { + let acct_state: AccountState = + bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + if let AccountState::Unallocated = acct_state { + Ok(()) + } else { + error!("Provided account is already occupied"); + Err(InstructionError::InvalidAccountData) + } + } + + pub fn do_client_request( + keyed_accounts: &mut [KeyedAccount], + request_info: &ClientRequestInfo, + ) -> Result<(), InstructionError> { + if keyed_accounts.len() != 2 { + error!("Client Request invalid accounts argument length (should be 2)") + } + const OWNER_INDEX: usize = 0; + const REQUEST_INDEX: usize = 1; + + // check_account_unallocated(&keyed_accounts[REQUEST_INDEX].account.data)?; + Ok(()) //placeholder + } + + pub fn do_cancel_request(keyed_accounts: &mut [KeyedAccount]) -> Result<(), InstructionError> { + if keyed_accounts.len() != 2 { + error!("Client Request invalid accounts argument length (should be 2)") + } + const OWNER_INDEX: usize = 0; + const CANCEL_INDEX: usize = 1; + Ok(()) //placeholder + } + + pub fn do_submit_proof( + keyed_accounts: &mut [KeyedAccount], + proof_info: &Proof, + ) -> Result<(), InstructionError> { + if keyed_accounts.len() != 2 { + error!("Client Request invalid accounts argument length (should be 2)") + } + const SUBMITTER_INDEX: usize = 0; + const PROOF_REQUEST_INDEX: usize = 1; + Ok(()) //placeholder + } +} +pub fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], +) -> Result<(), InstructionError> { + // solana_logger::setup(); + + let command = bincode::deserialize::(data).map_err(|err| { + info!("invalid instruction data: {:?} {:?}", data, err); + InstructionError::InvalidInstructionData + })?; + + trace!("{:?}", command); + + match command { + SpvInstruction::ClientRequest(client_request_info) => { + SpvProcessor::do_client_request(keyed_accounts, &client_request_info) + } + SpvInstruction::CancelRequest => SpvProcessor::do_cancel_request(keyed_accounts), + SpvInstruction::SubmitProof(proof_info) => { + SpvProcessor::do_submit_proof(keyed_accounts, &proof_info) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{spv_instruction, spv_state, utils}; + + #[test] + fn test_parse_header_hex() -> Result<(), SpvError> { + let testheader = "010000008a730974ac39042e95f82d719550e224c1a680a8dc9e8df9d007000000000000f50b20e8720a552dd36eb2ebdb7dceec9569e0395c990c1eb8a4292eeda05a931e1fce4e9a110e1a7a58aeb0"; + let testhash = "0000000000000bae09a7a393a8acded75aa67e46cb81f7acaa5ad94f9eacd103"; + let testheaderbytes = decode_hex(&testheader)?; + let testhashbytes = decode_hex(&testhash)?; + + let mut blockhash: [u8; 32] = [0; 32]; + blockhash.copy_from_slice(&testhashbytes[..32]); + + let mut version: [u8; 4] = [0; 4]; + version.copy_from_slice(&testheaderbytes[..4]); + let test_version = u32::from_le_bytes(version); + + let mut test_parent: [u8; 32] = [0; 32]; + test_parent.copy_from_slice(&testheaderbytes[4..36]); + + let mut merkleroot: [u8; 32] = [0; 32]; + merkleroot.copy_from_slice(&testheaderbytes[36..68]); + + let mut time: [u8; 4] = [0; 4]; + time.copy_from_slice(&testheaderbytes[68..72]); + let test_time = u32::from_le_bytes(time); + + let mut test_nonce: [u8; 4] = [0; 4]; + test_nonce.copy_from_slice(&testheaderbytes[76..80]); + + let bh = BlockHeader::hexnew(&testheader, &testhash)?; + + assert_eq!(bh.blockhash, blockhash); + assert_eq!(bh.merkle_root.hash, merkleroot); + assert_eq!(bh.version, test_version); + assert_eq!(bh.time, test_time); + assert_eq!(bh.parent, test_parent); + assert_eq!(bh.nonce, test_nonce); + + Ok(()) + } +} diff --git a/programs/btc_spv_api/src/spv_state.rs b/programs/btc_spv_api/src/spv_state.rs new file mode 100644 index 0000000000..1b3b184a70 --- /dev/null +++ b/programs/btc_spv_api/src/spv_state.rs @@ -0,0 +1,295 @@ +use crate::header_store::*; +use crate::utils::*; +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; +use std::{error, fmt}; + +pub type BitcoinTxHash = [u8; 32]; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct BlockHeader { + // Bitcoin network version + pub version: u32, + // Previous block's hash/digest + pub parent: [u8; 32], + // merkle Root of the block, proofEntry side should be None + pub merkle_root: ProofEntry, + // the blocktime associate with the block + pub time: u32, + // An encoded version of the target threshold this block’s header hash must be less than or equal to. + pub nbits: [u8; 4], + // block header's nonce + pub nonce: [u8; 4], + // Block hash + pub blockhash: [u8; 32], +} + +impl BlockHeader { + pub fn new(header: &[u8; 80], blockhash: &[u8; 32]) -> Result { + let mut va: [u8; 4] = [0; 4]; + va.copy_from_slice(&header[0..4]); + let version = u32::from_le_bytes(va); + + let mut ph: [u8; 32] = [0; 32]; + ph.copy_from_slice(&header[4..36]); + let parent = ph; + // extract merkle root in internal byte order + let mut mrr: [u8; 32] = [0; 32]; + mrr.copy_from_slice(&header[36..68]); + let merkle_root = ProofEntry { + hash: mrr, + side: EntrySide::Root, + }; + // timestamp associate with the block + let mut bt: [u8; 4] = [0; 4]; + bt.copy_from_slice(&header[68..72]); + let time = u32::from_le_bytes(bt); + + // nbits field is an encoded version of the + let mut nb: [u8; 4] = [0; 4]; + nb.copy_from_slice(&header[72..76]); + let nbits = nb; + + let mut nn: [u8; 4] = [0; 4]; + nn.copy_from_slice(&header[76..80]); + let nonce = nn; + + let bh = BlockHeader { + version, + parent, + merkle_root, + time, + nbits, + nonce, + blockhash: *blockhash, + }; + Ok(bh) + } + + pub fn hexnew(header: &str, blockhash: &str) -> Result { + if header.len() != 160 || blockhash.len() != 64 { + return Err(SpvError::InvalidBlockHeader); + } + + match decode_hex(header) { + Ok(header) => { + let bhbytes = decode_hex(blockhash)?; + const SIZE: usize = 80; + let mut hh = [0; SIZE]; + hh.copy_from_slice(&header[..header.len()]); + + let mut bhb: [u8; 32] = [0; 32]; + bhb.copy_from_slice(&bhbytes[..bhbytes.len()]); + + Ok(BlockHeader::new(&hh, &bhb).unwrap()) + } + Err(e) => Err(SpvError::InvalidBlockHeader), + } + } + + pub fn difficulty(mut self) -> u32 { + // calculates difficulty from nbits + let standin: u32 = 123_456_789; + standin + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Transaction { + inputs: Vec, + //input utxos + outputs: Vec, + //output utxos + version: u32, + //bitcoin network version + locktime: u32, +} + +// impl Transaction { +// fn new(bytes: Vec) -> Self { +// //reinsert later +// } +// fn hexnew(hex: String) -> Self { +// //reinsert later +// } +// } + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Input { + r#type: InputType, + // Type of the input + position: u32, + // position of the tx in its Block + txhash: BitcoinTxHash, + // hash of the transaction +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum InputType { + LEGACY, + COMPATIBILITY, + WITNESS, + NONE, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Output { + r#type: OutputType, + // type of the output + value: u64, + // amount of btc in sats + payload: Vec, // data sent with the transaction +} + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum OutputType { + WPKH, + WSH, + OP_RETURN, + PKH, + SH, + NONSTANDARD, +} + +pub type HeaderChain = Vec; +// a vector of BlockHeaders used as part of a Proof +// index 0 : the block header of the block prior to the proof Block +// index 1 : the block header of the proof block +// index 2-n* : the block headers for the confirmation chain +// (where n is the confirmations value from the proof request) + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct ProofEntry { + // 32 byte merkle hashes + pub hash: [u8; 32], + // side of the merkle tree entry + pub side: EntrySide, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum EntrySide { + // Left side of the hash combination + Left, + // Right side of hash combination + Right, + // Root hash (neither side) + Root, +} + +pub type MerkleProof = Vec; +// a vector of ProofEntries used as part of a Proof +// index 0 : a ProofEntry representing the txid +// indices 0-n : ProofEntries linking the txhash and the merkle root +// index n : a ProofEntry representing the merkel root for the block in question + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct ClientRequestInfo { + // bitcoin transaction hash + pub txhash: BitcoinTxHash, + // confirmation count + pub confirmations: u8, + // fee paid for tx verification + pub fee: u64, + // required minimum difficulty for submitted blocks + pub difficulty: u64, + // expiration slot height + pub expiration: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct ProofRequest { + pub owner: Pubkey, + // bitcoin transaction hash + pub txhash: BitcoinTxHash, + // confirmation count + pub confirmations: u8, + // fee paid for tx verification + pub fee: u64, + // minimum allowable difficulty + pub difficulty: u64, + // expiration slot height + pub expiration: u64, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Proof { + // the pubkey who submitted the proof in question, entitled to fees from any corresponding proof requests + pub submitter: Pubkey, + // merkle branch connecting txhash to block header merkle root + pub proof: MerkleProof, + // chain of bitcoin headers provifing context for the proof + pub headers: HeaderChain, + // transaction associated with the Proof + pub transaction: Transaction, + // public key of the request this proof corresponds to + pub request: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum AccountState { + // Request Account + Request(ClientRequestInfo), + // Verified Proof + Verification(Proof), + // Account's userdata is Unallocated + Unallocated, + // Invalid + Invalid, +} + +impl Default for AccountState { + fn default() -> Self { + AccountState::Unallocated + } +} + +///Errors +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum SpvError { + InvalidBlockHeader, + // blockheader is malformed or out of order + HeaderStoreError, + // header store write/read result is invalid + ParseError, + // other errors with parsing inputs +} + +impl error::Error for SpvError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + // temporary measure + None + } +} + +impl From for SpvError { + fn from(e: HeaderStoreError) -> Self { + SpvError::HeaderStoreError + } +} + +impl From for SpvError { + fn from(e: DecodeHexError) -> Self { + SpvError::ParseError + } +} + +// impl fmt::Debug for SpvError { +// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result{ +// match self { +// SpvError::InvalidBlockHeader => "BlockHeader is malformed or does not apply ".fmt(f), +// SpvError::HeaderStoreError => "Placeholder headerstore error debug text".fmt(f), +// SpvError::ParseError => "Error parsing blockheaders debug".fmt(f), +// } +// } +// } + +impl fmt::Display for SpvError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SpvError::InvalidBlockHeader => "BlockHeader is malformed or does not apply ".fmt(f), + SpvError::HeaderStoreError => "Placeholder headerstore error text".fmt(f), + SpvError::ParseError => "Error parsing blockheaders placceholder text".fmt(f), + } + } +} diff --git a/programs/btc_spv_api/src/utils.rs b/programs/btc_spv_api/src/utils.rs new file mode 100644 index 0000000000..ceb0466846 --- /dev/null +++ b/programs/btc_spv_api/src/utils.rs @@ -0,0 +1,33 @@ +use std::{fmt, num::ParseIntError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DecodeHexError { + OddLength, + ParseInt(ParseIntError), +} + +impl From for DecodeHexError { + fn from(e: ParseIntError) -> Self { + DecodeHexError::ParseInt(e) + } +} + +impl fmt::Display for DecodeHexError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DecodeHexError::OddLength => "input hex string length is odd ".fmt(f), + DecodeHexError::ParseInt(e) => e.fmt(f), + } + } +} + +pub fn decode_hex(s: &str) -> Result, DecodeHexError> { + if s.len() % 2 != 0 { + Err(DecodeHexError::OddLength) + } else { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.into())) + .collect() + } +} diff --git a/programs/btc_spv_bin/.gitignore b/programs/btc_spv_bin/.gitignore new file mode 100644 index 0000000000..5404b132db --- /dev/null +++ b/programs/btc_spv_bin/.gitignore @@ -0,0 +1,2 @@ +/target/ +/farf/ diff --git a/programs/btc_spv_bin/Cargo.toml b/programs/btc_spv_bin/Cargo.toml new file mode 100644 index 0000000000..c0ee392fdb --- /dev/null +++ b/programs/btc_spv_bin/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "btc_spv_bin" +version = "0.1.0" +authors = ["patrick "] +edition = "2018" + +[dependencies] +reqwest="0.9.19" +clap="2.33.0" +serde_derive="1.0.98" +serde="1.0.99" + +[[bin]] +name = "blockheaders" +path = "src/blockheade.rs" diff --git a/programs/btc_spv_bin/src/blockheade.rs b/programs/btc_spv_bin/src/blockheade.rs new file mode 100644 index 0000000000..894cad83a7 --- /dev/null +++ b/programs/btc_spv_bin/src/blockheade.rs @@ -0,0 +1,76 @@ +use clap; +use clap::{App, Arg}; +use reqwest; +use serde::Deserialize; + +// pub type blockHash = [u8; 32]; +pub type BlockHeader = [u8; 80]; + +#[allow(dead_code)] +#[derive(Deserialize)] +struct JsonBH { + hash: String, + ver: u16, + prev_block: String, + mrkl_root: String, + time: u64, + bits: u64, + nonce: u64, + n_tx: u64, + size: u64, + block_index: u64, + main_chain: bool, + height: u64, + received_time: u64, + relayed_by: String, +} + +#[allow(dead_code)] +fn get_header_json(hash: &str) -> JsonBH { + let qs = format!("https://www.blockchain.info/rawblock/{}", hash); + let body = reqwest::get(&qs); + match body { + Err(e) => panic!("rest request failed {}", e), + Ok(mut n) => { + if n.status().is_success() { + let jsonbh: JsonBH = n.json().unwrap(); + jsonbh + } else { + panic!("request failed"); + } + } + } +} + +fn get_header_raw(hash: &str) -> String { + let qs = format!("https://blockchain.info/block/{}?format=hex", hash); + let body = reqwest::get(&qs); + match body { + Err(e) => panic!("rest request failed {}", e), + Ok(mut n) => { + if n.status().is_success() { + let textbh: String = n.text().unwrap(); + let hs = &textbh[0..160]; // 160 characters since it's in hex format + let header: String = hs.to_string(); + header + } else { + panic!("request failed"); + } + } + } +} + +fn main() { + let matches = App::new("header fetch util") + .arg(Arg::with_name("blockhash")) + .help("block hash to get header from") + .get_matches(); + + let testhash = "0000000000000bae09a7a393a8acded75aa67e46cb81f7acaa5ad94f9eacd103"; + let blockhash = matches.value_of("blockhash").unwrap_or(testhash); + let headerraw = get_header_raw(&blockhash); + println!("header - {}", headerraw); + println!("hash - {}", blockhash); + println!("length - {}", headerraw.len()); + // println!("{}", std::str::from_utf8(&header)); +} diff --git a/programs/btc_spv_program/.gitignore b/programs/btc_spv_program/.gitignore new file mode 100644 index 0000000000..5404b132db --- /dev/null +++ b/programs/btc_spv_program/.gitignore @@ -0,0 +1,2 @@ +/target/ +/farf/ diff --git a/programs/btc_spv_program/Cargo.toml b/programs/btc_spv_program/Cargo.toml new file mode 100644 index 0000000000..321110736a --- /dev/null +++ b/programs/btc_spv_program/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "solana-bitcoin-spv-program" +version = "0.19.0-pre0" +description = "Solana Bitcoin spv parsing program" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +bincode = "1.1.4" +chrono = { version = "0.4.0", features = ["serde"] } +log = "0.4.2" +num-derive = "0.2" +num-traits = "0.2" +serde = "1.0.99" +serde_derive = "1.0.98" +solana-sdk = { path = "../../sdk", version = "0.19.0-pre0"} +solana-btc-spv-api = { path = "../btc_spv_api", version = "0.19.0-pre0"} + + +[lib] +crate-type = ["lib", "cdylib"] +name = "solana_btc_spv_program" diff --git a/programs/btc_spv_program/src/lib.rs b/programs/btc_spv_program/src/lib.rs new file mode 100644 index 0000000000..4b1a6b70c1 --- /dev/null +++ b/programs/btc_spv_program/src/lib.rs @@ -0,0 +1,13 @@ +use solana_btc_spv_api::spv_processor::process_instruction; + +#[macro_export] +macro_rules! solana_btc_spv_program { + () => { + ( + "solana_btc_spv_program".to_string(), + solana_btc_spv_api::id(), + ) + }; +} + +solana_sdk::solana_entrypoint!(process_instruction);