diff --git a/Cargo.toml b/Cargo.toml index b903b73c5..c3cb84a4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nomic" version = "8.0.0" -authors = ["The Turbofish Team "] +authors = ["Nomic DAO Foundation "] edition = "2021" default-run = "nomic" @@ -47,7 +47,6 @@ ics23 = "0.12.0" cosmos-sdk-proto = { version = "0.23.0", optional = true } prometheus_exporter = "0.8.5" lazy_static = "1.4.0" -getrandom = { version = "0.2.15", features = ["js"] } [dev-dependencies] bitcoind = { version = "0.27.0", features = ["22_0"] } diff --git a/README.md b/README.md index acf18395f..07a7e1aa8 100644 --- a/README.md +++ b/README.md @@ -1,198 +1,55 @@

- + + + + Nomic +

+

+Decentralized Custody Engine for Bitcoin +

-Nomic Bitcoin Bridge +![CI](https://github.com/nomic-io/nomic/actions/workflows/ci.yml/badge.svg) -## Testnet Interchain Upgrade +Nomic is a blockchain which offers a decentralized custody solution for Bitcoin. Built on Turbofish’s [Orga](https://github.com/turbofish-org/orga), a custom high-performance blockchain application framework. Nomic mints nBTC, a token backed 1:1 with BTC, using [IBC](https://www.ibcprotocol.dev/) for secure and efficient bridging. -This testnet version is the release candidate for the upcoming Stakenet upgrade. -## Upgrading existing nodes +## Running a Node -If you're upgrading your existing testnet node: +Running a node increases the health of the network by decentralizing ledger validation and data, even for non-validator nodes. Community members are encouraged to run a node, especially when regularly interacting with the network via transactions and queries. -1. Rebuild from this branch with: +[Nomic Network Docs](https://docs.nomic.io/) -``` -git pull +## Integrating with Nomic -cargo install --locked --path . -``` +Integrating with nBTC enables accepting Bitcoin deposits with Interchain Deposits to any IBC-enabled blockchain. -2. Shut down your running node. +[nBTC Docs](https://github.com/nomic-io/nomic-bitcoin-js/blob/main/README.md) -3. Restart your node with `nomic start`. +## Contributing -Your node will automatically perform the upgrade on Friday, October 7 at 17:00 UTC. +Nomic is an open-source project spearheaded by contributors. Anyone is able to contribute to Nomic via GitHub. -## Node setup guide +[Contribute to Nomic](https://github.com/nomic-io/nomic/contribute) -This guide will walk you through setting up a node for the Nomic testnet. +## Security -If you need any help getting your node running, join the [Discord](https://discord.gg/jH7U2NRJKn) and ask for the Validator role. +Nomic is currently undergoing security audits. -### Requirements +Vulnerabilities should not be reported through public channels, including GitHub Issues. You can report a vunerability via GitHub's Private Vunerability Reporting or via the Nomic DAO Foundation at `foundation@nomic.io`. -- >= 4GB RAM -- >= 100GB of storage -- Linux or macOS _(Windows support coming soon)_ +[Report a Vulnerability](https://github.com/nomic-io/nomic/security/advisories/new) -### 1. Build Nomic -Start by building Nomic - for now this requires Rust nightly. -Install rustup if you haven't already: -``` -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` -Install nightly as well (nomic currently requires rust nightly): -``` -rustup default nightly -``` +## License -Install required dependencies (ubuntu): -``` -sudo apt install build-essential libssl-dev pkg-config clang -``` +Licensed under the Apache License, Version 2.0 (the "License"); you may not use the files in this repository except in compliance with the License. You may obtain a copy of the License at -For systems running fedora: -``` -sudo dnf install clang openssl-devel && sudo dnf group install "C Development Tools and Libraries" -``` + https://www.apache.org/licenses/LICENSE-2.0 -Clone the github folder and switch to the correct folder: -``` -git clone https://github.com/nomic-io/nomic.git && cd nomic -git checkout testnet -``` +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -Build and install, adding a `nomic` command to your PATH: -``` -cargo install --locked --path . -``` - -### 2. Run your node -Start your Nomic node: -``` -nomic start -``` - -This will run the Nomic state machine and a Tendermint process. For new nodes the statesync process will run automatically to get the node up to speed with the current chain. - -### 3. Acquiring coins and staking for voting power - -First, find your address by running `nomic balance` (for now this must be run on -the same machine as your active full node). - -Ask the Nomic team for some coins in the Discord and include your address. - -Once you have received coins, you can declare your node as a validator and -delegate to yourself with: - -``` -nomic declare \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -
-``` - -**IMPORTANT NOTE:** Carefully double-check all the fields since you will not be -able to modify the `commission_max` or `commission_max_change` after declaring. If you make a mistake, you will have to -declare a new validator instead. - -- The `validator_consensus_key` field is the base64 pubkey `value` field found - under `"validator_info"` in the output of http://localhost:26657/status. -- The `identity` field is the 64-bit hex key suffix found on your Keybase - profile, used to get your profile picture in wallets and block explorers. - -For example: - -``` -nomic declare \ - ohFOw5u9LGq1ZRMTYZD1Y/WrFtg7xfyBaEB4lSgfeC8= \ - 100000 \ - 0.042 \ - 0.1 \ - 0.01 \ - 100000 \ - "Foo's Validator" \ - "https://foovalidator.com" \ - 37AA68F6AA20B7A8 \ - "Please delegate to me!" -``` - -### 4. Run your Bitcoin signer - -The funds in the Bitcoin bridge are held in a large multisig controlled by the Nomic validators. If you are a validator with a significant amount of voting power, it is very important that you run a signer. - -#### i. Set your signatory key - -This will submit your public key to the network so you can be added to the multisig. If you do not have a key stored at `~/.nomic-testnet-4c/signer/xpriv`, this will automatically generate a Bitcoin extended private key for you. **KEEP THIS KEY SAFE** - similar to your validator private key, it is important to be mindful of this key so that it is never lost or stolen. - -**Note:** Setting your signatory key is only required if you are starting a fresh node. Migrating nodes can move on to the next step. - -``` -nomic set-signatory-key -``` - -If you have your extended private key stored in a different location than the default, you may pass a path. - -``` -nomic set-signatory-key -``` - -#### ii. Run your Bitcoin signer - -You can run the signer with: - -``` -nomic signer -``` - -If you have stored your xpriv in a different location, you can pass the path to the signer. - -``` -nomic signer xpriv_paths=[] -``` - -Leave this process running, it will automatically sign Bitcoin transactions that the network wants to create. - -In the future, we hope for the community to come up with alternative types of signers which provide for extra security, by e.g. airgapping keys, using HSMs, or prompting the user for an encryption key. - -### 5. (Optional) Run a relayer - -Relayer nodes carry data between the Bitcoin blockchain and the Nomic blockchain. You can help support the health of the network by running a Bitcoin node alongside your Nomic node and running the relayer process. - -#### i. Sync a Bitcoin node - -Download Bitcoin Core: https://bitcoin.org/en/download - -Run it with: -``` -bitcoind -server -testnet -rpcuser=satoshi -rpcpassword=nakamoto -``` -(The RPC server only listens on localhost, so the user and password are not critically important.) - -**NOTE:** To save on disk space, you may want to configure your Bitcoin node to prune block storage. For instance, add `-prune=5000` to only keep a maximum of 5000 MB of blocks. You may also want to use the `-daemon` option to keep the node running in the background. - -#### ii. Run the relayer process - -``` -nomic relayer --rpc-port=18332 --rpc-user=satoshi --rpc-pass=nakamoto -``` - -Leave this running - the relayer will constantly scan the Bitcoin and Nomic chains and broadcast relevant data. - -The relayer will also create a server which listens on port 8999 for clients to announce their deposit addresses. To help make the network more reliable, if you run a relayer please open this port and let us know your node's address in Discord or a Github issue so we can have clients make use of your node. If you're going to make this service public, putting the server behind an HTTP reverse proxy is recommended for extra safety. --- -Thanks for participating in the Nomic Testnet! We'll be updating the network -often so stay tuned in [Discord](https://discord.gg/jH7U2NRJKn) for updates. +Copyright © 2024 Nomic DAO Foundation. diff --git a/logo.svg b/logo.svg deleted file mode 100644 index 2de142ddd..000000000 --- a/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/nomic-logo-100.png b/nomic-logo-100.png new file mode 100644 index 000000000..481bdabc1 Binary files /dev/null and b/nomic-logo-100.png differ diff --git a/nomic-logo-dark-100.png b/nomic-logo-dark-100.png new file mode 100644 index 000000000..828f857d4 Binary files /dev/null and b/nomic-logo-dark-100.png differ diff --git a/src/app.rs b/src/app.rs index 47ce07b7b..7422b85b8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use crate::airdrop::Airdrop; use crate::bitcoin::adapter::Adapter; +use crate::bitcoin::matches_bitcoin_network; use crate::bitcoin::{Bitcoin, Nbtc}; use crate::cosmos::{Chain, Cosmos, Proof}; @@ -337,6 +338,7 @@ impl InnerApp { match dest { Dest::Address(addr) => self.bitcoin.accounts.deposit(addr, nbtc), Dest::Ibc(dest) => dest.transfer(nbtc, &mut self.bitcoin, &mut self.ibc), + Dest::Fee => Ok(self.bitcoin.give_rewards(nbtc)?), } } @@ -462,6 +464,13 @@ impl FromStr for NbtcMemo { } let dest = parts[1]; let script = if let Ok(addr) = bitcoin::Address::from_str(dest) { + if !matches_bitcoin_network(&addr.network) { + return Err(Error::App(format!( + "Invalid network for nBTC memo. Got {}, Expected {}", + addr.network, + crate::bitcoin::NETWORK + ))); + } addr.script_pubkey() } else { bitcoin::Script::from_str(parts[1]).map_err(|e| Error::App(e.to_string()))? @@ -526,7 +535,6 @@ mod abci { impl BeginBlock for InnerApp { fn begin_block(&mut self, ctx: &BeginBlockCtx) -> Result<()> { - self.bitcoin.checkpoints.config.max_unconfirmed_checkpoints = 100; let now = ctx.header.time.as_ref().unwrap().seconds; self.upgrade.step( &vec![Self::CONSENSUS_VERSION].try_into().unwrap(), @@ -890,6 +898,14 @@ impl ConvertSdkTx for InnerApp { let dest_addr: bitcoin::Address = msg.dst_address.parse().map_err( |e: bitcoin::util::address::Error| Error::App(e.to_string()), )?; + if !matches_bitcoin_network(&dest_addr.network) { + return Err(Error::App(format!( + "Invalid network for destination address. Got {}, Expected {}", + dest_addr.network, + crate::bitcoin::NETWORK + ))); + } + let dest_script = crate::bitcoin::adapter::Adapter::new(dest_addr.script_pubkey()); @@ -1016,6 +1032,14 @@ impl ConvertSdkTx for InnerApp { .parse() .map_err(|_| Error::App("Invalid recovery address".to_string()))?; + if !matches_bitcoin_network(&recovery_addr.network) { + return Err(Error::App(format!( + "Invalid network for recovery address. Got {}, Expected {}", + recovery_addr.network, + crate::bitcoin::NETWORK + ))); + } + let script = crate::bitcoin::adapter::Adapter::new(recovery_addr.script_pubkey()); @@ -1073,13 +1097,15 @@ pub struct MsgIbcTransfer { pub enum Dest { Address(Address), Ibc(IbcDest), + Fee, } impl Dest { - pub fn to_receiver_addr(&self) -> String { + pub fn to_receiver_addr(&self) -> Option { match self { - Dest::Address(addr) => addr.to_string(), - Dest::Ibc(dest) => dest.receiver.0.to_string(), + Dest::Address(addr) => Some(addr.to_string()), + Dest::Ibc(dest) => Some(dest.receiver.0.to_string()), + Dest::Fee => None, } } } @@ -1170,6 +1196,7 @@ impl Dest { let bytes = match self { Address(addr) => addr.bytes().into(), Ibc(dest) => Sha256::digest(dest.encode()?).to_vec(), + Fee => vec![1], }; Ok(bytes) diff --git a/src/bin/nomic.rs b/src/bin/nomic.rs index af14b0294..dce6f098c 100644 --- a/src/bin/nomic.rs +++ b/src/bin/nomic.rs @@ -20,12 +20,12 @@ use nomic::app::IbcDest; use nomic::app::InnerApp; use nomic::app::Nom; use nomic::bitcoin::adapter::Adapter; +use nomic::bitcoin::matches_bitcoin_network; use nomic::bitcoin::signatory::SignatorySet; use nomic::bitcoin::Nbtc; use nomic::bitcoin::{relayer::Relayer, signer::Signer}; use nomic::error::Result; -use nomic::utils::load_bitcoin_key; -use nomic::utils::load_or_generate; +use nomic::utils::{load_bitcoin_key, load_or_generate}; use orga::abci::Node; use orga::client::wallet::{SimpleWallet, Wallet}; use orga::coins::{Address, Commission, Decimal, Declaration, Symbol}; @@ -1521,7 +1521,8 @@ async fn deposit( )) }) .await?; - let script = sigset.output_script(dest.commitment_bytes()?.as_slice(), threshold)?; + let commitment_bytes = dest.commitment_bytes()?; + let script = sigset.output_script(&commitment_bytes, threshold)?; let btc_addr = bitcoin::Address::from_script(&script, nomic::bitcoin::NETWORK).unwrap(); let mut successes = 0; @@ -1638,6 +1639,13 @@ impl WithdrawCmd { /// Runs the `withdraw` command. async fn run(&self) -> Result<()> { let script = self.dest.script_pubkey(); + if !matches_bitcoin_network(&self.dest.network) { + return Err(nomic::error::Error::Address(format!( + "Invalid network for destination address. Got {}, Expected {}", + self.dest.network, + nomic::bitcoin::NETWORK + ))); + } self.config .client() @@ -2047,6 +2055,14 @@ impl SetRecoveryAddressCmd { /// Runs the `set-recovery-address` command. async fn run(&self) -> Result<()> { let script = self.address.script_pubkey(); + if !matches_bitcoin_network(&self.address.network) { + return Err(nomic::error::Error::Address(format!( + "Invalid network for recovery address. Got {}, Expected {}", + self.address.network, + nomic::bitcoin::NETWORK + ))); + } + Ok(self .config .client() diff --git a/src/bitcoin/checkpoint.rs b/src/bitcoin/checkpoint.rs index 7817bc52d..95fd4c099 100644 --- a/src/bitcoin/checkpoint.rs +++ b/src/bitcoin/checkpoint.rs @@ -267,8 +267,12 @@ impl BitcoinTx { /// The estimated size of the transaction, including the worst-case sizes of /// all input witnesses once fully signed, in virtual bytes. - pub fn vsize(&self) -> Result { - Ok(self.to_bitcoin_tx()?.vsize().try_into()?) + pub fn est_vsize(&self) -> Result { + let base_vsize: u64 = self.to_bitcoin_tx()?.vsize().try_into()?; + let est_witness_vsize = self.input.iter()?.try_fold(0, |sum: u64, input| { + Ok::<_, Error>(sum + input?.est_witness_vsize) + })?; + Ok(base_vsize + est_witness_vsize) } /// The hash of the transaction. Note that this will change if any inputs or @@ -1304,7 +1308,7 @@ impl<'a> BuildingCheckpointMut<'a> { .get_mut(BatchType::IntermediateTx as u64)? .unwrap(); let mut intermediate_tx = intermediate_tx_batch.get_mut(0)?.unwrap(); - let fee = intermediate_tx.vsize()? * fee_rate; + let fee = intermediate_tx.est_vsize()? * fee_rate; intermediate_tx.deduct_fee(fee)?; fee }; @@ -1367,7 +1371,9 @@ impl<'a> BuildingCheckpointMut<'a> { // Deduct the final tx's miner fee from its outputs, // removing any outputs which are too small to pay their // share of the fee. - let tx_size = tx.vsize().map_err(|err| OrgaError::App(err.to_string()))?; + let tx_size = tx + .est_vsize() + .map_err(|err| OrgaError::App(err.to_string()))?; let fee = intermediate_tx_fee / intermediate_tx_len + tx_size * fee_rate; tx.deduct_fee(fee) .map_err(|err| OrgaError::App(err.to_string()))?; @@ -1480,7 +1486,7 @@ impl<'a> BuildingCheckpointMut<'a> { // and add our output there instead. // TODO: don't pop and repush, just get a mutable reference let mut curr_tx = final_txs.pop().unwrap(); - if curr_tx.vsize()? >= config.emergency_disbursal_max_tx_size { + if curr_tx.est_vsize()? >= config.emergency_disbursal_max_tx_size { self.link_intermediate_tx(&mut curr_tx, config.sigset_threshold)?; final_txs.push(curr_tx); curr_tx = BitcoinTx::with_lock_time(lock_time); diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index ff2d3944f..1a467948a 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -256,6 +256,18 @@ impl Default for Config { } } +pub fn matches_bitcoin_network(network: &bitcoin::Network) -> bool { + match crate::bitcoin::NETWORK { + bitcoin::Network::Bitcoin => network == &crate::bitcoin::NETWORK, + bitcoin::Network::Regtest => { + network == &bitcoin::Network::Regtest || network == &bitcoin::Network::Testnet + } + bitcoin::Network::Testnet | bitcoin::Network::Signet => { + network == &bitcoin::Network::Testnet || network == &bitcoin::Network::Signet + } + } +} + /// Calculates the bridge fee for a deposit of the given amount of BTC, in /// satoshis. pub fn calc_deposit_fee(amount: u64) -> u64 { @@ -478,10 +490,7 @@ impl Bitcoin { )) })?; - let regtest_mode = self.network() == bitcoin::Network::Regtest - && _signatory_key.network == bitcoin::Network::Testnet; - - if !regtest_mode && _signatory_key.network != self.network() { + if !matches_bitcoin_network(&_signatory_key.network) { return Err(Error::Orga(orga::Error::App( "Signatory key network does not match network".to_string(), ))); @@ -594,7 +603,6 @@ impl Bitcoin { let checkpoint = self.checkpoints.get(sigset_index)?; let sigset = checkpoint.sigset.clone(); - let dest_bytes = dest.commitment_bytes()?; let expected_script = sigset.output_script(&dest_bytes, self.checkpoints.config.sigset_threshold)?; @@ -665,7 +673,9 @@ impl Bitcoin { // TODO: keep in excess queue if full let deposit_fee = nbtc.take(calc_deposit_fee(nbtc.amount.into()))?; - self.give_rewards(deposit_fee)?; + self.checkpoints + .building_mut()? + .insert_pending(Dest::Fee, deposit_fee)?; self.checkpoints .building_mut()? diff --git a/src/bitcoin/recovery.rs b/src/bitcoin/recovery.rs index 0ff1678cd..9b35f8c70 100644 --- a/src/bitcoin/recovery.rs +++ b/src/bitcoin/recovery.rs @@ -54,17 +54,18 @@ impl RecoveryTxs { .output .get(args.vout as usize) .ok_or_else(|| Error::Signer("Invalid recovery tx vout".to_string()))?; + let commitment_bytes = args.dest.commitment_bytes()?; let input = Input::new( OutPoint::new(args.expired_tx.txid(), args.vout), args.old_sigset, - &args.dest.commitment_bytes()?, + &commitment_bytes, expired_output.value, args.threshold, )?; let script_pubkey = args .new_sigset - .output_script(args.dest.commitment_bytes()?.as_slice(), args.threshold)?; + .output_script(&commitment_bytes, args.threshold)?; let output = TxOut { value: expired_output.value, script_pubkey, @@ -74,7 +75,7 @@ impl RecoveryTxs { tx.input.push_back(input)?; tx.output.push_back(Adapter::new(output))?; - tx.deduct_fee(args.fee_rate * tx.vsize()?)?; + tx.deduct_fee(args.fee_rate * tx.est_vsize()?)?; tx.populate_input_sig_message(0)?; diff --git a/src/bitcoin/relayer.rs b/src/bitcoin/relayer.rs index 86ab94c58..b98f4d30c 100644 --- a/src/bitcoin/relayer.rs +++ b/src/bitcoin/relayer.rs @@ -10,7 +10,6 @@ use crate::error::Result; use crate::orga::encoding::Encode; use crate::utils::time_now; use bitcoin::consensus::{Decodable, Encodable}; -use bitcoin::hashes::hex::FromHex; use bitcoin::Txid; use bitcoin::{hashes::Hash, Block, BlockHash, Transaction}; use bitcoincore_rpc_async::{json::GetBlockHeaderResult, Client as BitcoinRpcClient, RpcApi}; @@ -434,8 +433,12 @@ impl Relayer { )?; let mut index = index.lock().await; + let receiver_addr = match dest.to_receiver_addr() { + Some(addr) => addr, + None => continue, + }; index.insert_deposit( - dest.to_receiver_addr(), + receiver_addr, bitcoin_address, Deposit::new(txid, vout as u32, output.value, None), ) @@ -512,10 +515,6 @@ impl Relayer { pub async fn start_checkpoint_relay(&mut self) -> Result<()> { info!("Starting checkpoint relay..."); - let spk = - ::bitcoin::Script::from_hex("0014d982217f84fc7e88670ee87470defe1ad5ddb371").unwrap(); - let addr = ::bitcoin::Address::from_script(&spk, super::NETWORK).unwrap(); - println!("BITCOIN ADDRESS: {}", addr); loop { if let Err(e) = self.relay_checkpoints().await { if !e.to_string().contains("No completed checkpoints yet") { @@ -870,6 +869,10 @@ impl Relayer { let txid = tx.txid(); let outpoint = (txid.into_inner(), output.vout); let dest = output.dest.clone(); + if dest.to_receiver_addr().is_none() { + return Ok(()); + } + let receiver_addr = dest.to_receiver_addr().unwrap(); let vout = output.vout; let contains_outpoint = app_client(&self.app_client_addr) .query(|app| app.bitcoin.processed_outpoints.contains(outpoint)) @@ -882,13 +885,13 @@ impl Relayer { if contains_outpoint { let mut index = index.lock().await; - index.remove_deposit(dest.to_receiver_addr(), deposit_address, txid, vout)?; + index.remove_deposit(receiver_addr, deposit_address, txid, vout)?; return Ok(()); } let mut index_guard = index.lock().await; index_guard.insert_deposit( - dest.to_receiver_addr(), + receiver_addr, deposit_address.clone(), Deposit::new( txid, @@ -1193,7 +1196,7 @@ impl WatchedScripts { sigset: &SignatorySet, threshold: (u64, u64), ) -> Result<::bitcoin::Script> { - sigset.output_script(dest.commitment_bytes()?.as_slice(), threshold) + sigset.output_script(&dest.commitment_bytes()?, threshold) } } diff --git a/src/utils.rs b/src/utils.rs index 9acd22b19..7059797b3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -131,10 +131,11 @@ pub fn make_std_tx( pub fn generate_bitcoin_key(network: bitcoin::Network) -> Result { let seed: [u8; 32] = rand::thread_rng().gen(); - let network = if network == bitcoin::Network::Regtest { - bitcoin::Network::Testnet - } else { - network + let network = match network { + bitcoin::Network::Bitcoin => bitcoin::Network::Bitcoin, + bitcoin::Network::Testnet | bitcoin::Network::Signet | bitcoin::Network::Regtest => { + bitcoin::Network::Testnet + } }; Ok(ExtendedPrivKey::new_master(network, seed.as_slice())?) @@ -381,7 +382,7 @@ pub async fn poll_for_updated_balance(address: Address, expected_balance: u64) - .await .unwrap(); - if initial_balance <= expected_balance { + if initial_balance == expected_balance { return initial_balance.into(); } diff --git a/tests/bitcoin.rs b/tests/bitcoin.rs index 5e5850333..cf068cdbc 100644 --- a/tests/bitcoin.rs +++ b/tests/bitcoin.rs @@ -458,14 +458,6 @@ async fn bitcoin_test() { let balance = poll_for_updated_balance(funded_accounts[1].address, expected_balance).await; assert_eq!(balance, Amount::from(expected_balance)); - withdraw_bitcoin( - &funded_accounts[0], - bitcoin::Amount::from_sat(7000), - &withdraw_address, - ) - .await - .unwrap(); - app_client() .with_wallet(funded_accounts[0].wallet.clone()) .call( @@ -474,6 +466,14 @@ async fn bitcoin_test() { ) .await?; + withdraw_bitcoin( + &funded_accounts[0], + bitcoin::Amount::from_sat(7000), + &withdraw_address, + ) + .await + .unwrap(); + btc_client .generate_to_address(4, &async_wallet_address) .await @@ -541,7 +541,7 @@ async fn bitcoin_test() { } } } - assert_eq!(signatory_balance, 49994239); + assert_eq!(signatory_balance, 49993057); let funded_account_balances: Vec<_> = funded_accounts .iter() @@ -556,7 +556,7 @@ async fn bitcoin_test() { }) .collect(); - let expected_account_balances: Vec = vec![989980029, 0, 0, 0]; + let expected_account_balances: Vec = vec![989976483, 0, 0, 0]; assert_eq!(funded_account_balances, expected_account_balances); for (i, account) in funded_accounts[0..1].iter().enumerate() { diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 4ffaa2ac2..ca7ea7683 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -24,6 +24,7 @@ bitcoin = { version = "0.29.2", features = ["serde"] } urlencoding = "2.1.2" reqwest-wasm = {version = "=0.11.16", features = ["blocking"] } futures-lite = "1.13.0" +getrandom = {version = "0.2.15", features = ["js"] } [features] testnet = ["nomic/testnet"] diff --git a/wasm/src/web_client.rs b/wasm/src/web_client.rs index f3fb25286..04bc8f410 100644 --- a/wasm/src/web_client.rs +++ b/wasm/src/web_client.rs @@ -30,150 +30,108 @@ impl WebClient { impl Transport> for WebClient { async fn call(&self, _call: as Call>::Call) -> Result<()> { todo!() - // TODO: shouldn't need to deal with ABCIPlugin at this level - // let call = match call { - // ABCICall::DeliverTx(call) => call, - // _ => return Err(Error::Client("Unexpected call type".into())), - // }; - // let call_bytes = call.encode()?; - // let tx = base64::encode(&call_bytes); - // // let res = block_on(self.client.broadcast_tx_commit(call_bytes))?; - - // let window = match web_sys::window() { - // Some(window) => window, - // None => return Err(Error::App("Window not found".to_string())), - // }; - - // let storage = window - // .local_storage() - // .map_err(|_| Error::App("Could not get local storage".into()))? - // .unwrap(); - // let rest_server = storage - // .get("nomic/rest_server") - // .map_err(|_| Error::App("Could not load from local storage".into()))? - // .unwrap(); - - // let url = format!("{}/txs", rest_server); - - // // let request = Request::new_with_str_and_init(&url, &opts) - // // .map_err(|e| Error::App(format!("{:?}", e)))?; - - // // let resp_value = JsFuture::from(window.fetch_with_request(&request)) - // // .await - // // .map_err(|e| Error::App(format!("{:?}", e)))?; - - // // let res: Response = resp_value - // // .dyn_into() - // // .map_err(|e| Error::App(format!("{:?}", e)))?; - // // let res = JsFuture::from( - // // res.array_buffer() - // // .map_err(|e| Error::App(format!("{:?}", e)))?, - // // ) - // // .await - // // .map_err(|e| Error::App(format!("{:?}", e)))?; - // let client = reqwest_wasm::blocking::Client::new(); - // let res = client - // .post(url) - // .body(tx) - // .send() - // .map_err(|e| Error::App(format!("{:?}", e)))? - // .text() - // .map_err(|e| Error::App(format!("{:?}", e)))?; - // // let res = js_sys::Uint8Array::new(&res).to_vec(); - // // let res = String::from_utf8(res).map_err(|e| Error::App(format!("{:?}", e)))?; - - // #[cfg(feature = "logging")] - // web_sys::console::log_1(&format!("response: {}", &res).into()); - - // self.last_res - // .lock() - // .map_err(|e| Error::App(format!("{:?}", e)))? - // .replace(res); - - // // if let tendermint::abci::Code::Err(code) = res.check_tx.code { - // // let msg = format!("code {}: {}", code, res.check_tx.log); - // // return Err(Error::Call(msg)); - // // } - - // Ok(()) } async fn query(&self, query: T::Query) -> Result { - let query_bytes = query.encode()?; - let query = hex::encode(query_bytes); - let maybe_height: Option = self.height.lock().unwrap().map(Into::into); - - let window = match web_sys::window() { - Some(window) => window, - None => return Err(Error::App("Window not found".to_string())), - }; - - let storage = window - .local_storage() - .map_err(|_| Error::App("Could not get local storage".into()))? - .unwrap(); - let rest_server = storage - .get("nomic/rest_server") - .map_err(|_| Error::App("Could not load from local storage".into()))? - .unwrap(); - - let mut opts = RequestInit::new(); - opts.method("GET"); - opts.mode(RequestMode::Cors); - let mut url = format!("{}/query/{}", rest_server, query); - if let Some(height) = maybe_height { - url.push_str(&format!("?height={}", height)); - } - - let request = Request::new_with_str_and_init(&url, &opts) - .map_err(|e| Error::App(format!("{:?}", e)))?; - - let resp_value = JsFuture::from(window.fetch_with_request(&request)) - .await - .map_err(|e| Error::App(format!("{:?}", e)))?; - - let resp: Response = resp_value - .dyn_into() - .map_err(|e| Error::App(format!("{:?}", e)))?; - - let resp_buf = resp - .array_buffer() - .map_err(|e| Error::App(format!("{:?}", e)))?; - - let res = JsFuture::from(resp_buf) - .await - .map_err(|e| Error::App(format!("{:?}", e)))?; - let res = js_sys::Uint8Array::new(&res).to_vec(); - let res = String::from_utf8(res).map_err(|e| Error::App(format!("{:?}", e)))?; - let res = base64::decode(res).map_err(|e| Error::App(format!("{:?}", e)))?; - - // TODO: we shouldn't need to include the root hash in the result, it - // should come from a trusted source - let res_height = match res[0..4].try_into() { - Ok(inner) => u32::from_be_bytes(inner), - _ => panic!("Cannot convert result to fixed size array"), - }; - let mut height = self.height.lock().unwrap(); - if let Some(height) = height.as_ref() { - if *height != res_height { - return Err(Error::App(format!( - "Height mismatch: expected {}, got {}", - height, res_height - ))); + UnsafeSendFuture(async move { + let query_bytes = query.encode()?; + let query = hex::encode(query_bytes); + let maybe_height: Option = self.height.lock().unwrap().map(Into::into); + + let window = match web_sys::window() { + Some(window) => window, + None => return Err(Error::App("Window not found".to_string())), + }; + + let rest_server = { + let storage = window + .local_storage() + .map_err(|_| Error::App("Could not get local storage".into()))? + .unwrap(); + storage + .get("nomic/rest_server") + .map_err(|_| Error::App("Could not load from local storage".into()))? + .unwrap() + }; + + let resp_value = { + JsFuture::from(window.fetch_with_request(&{ + let mut opts = RequestInit::new(); + opts.method("GET"); + opts.mode(RequestMode::Cors); + let mut url = format!("{}/query/{}", rest_server, query); + if let Some(height) = maybe_height { + url.push_str(&format!("?height={}", height)); + } + + Request::new_with_str_and_init(&url, &opts) + .map_err(|e| Error::App(format!("{:?}", e)))? + })) + .await + .map_err(|e| Error::App(format!("{:?}", e)))? + }; + + let resp: Response = resp_value + .dyn_into() + .map_err(|e| Error::App(format!("{:?}", e)))?; + + let resp_buf = resp + .array_buffer() + .map_err(|e| Error::App(format!("{:?}", e)))?; + + let res = JsFuture::from(resp_buf) + .await + .map_err(|e| Error::App(format!("{:?}", e)))?; + let res = js_sys::Uint8Array::new(&res).to_vec(); + let res = String::from_utf8(res).map_err(|e| Error::App(format!("{:?}", e)))?; + let res = base64::decode(res).map_err(|e| Error::App(format!("{:?}", e)))?; + + // TODO: we shouldn't need to include the root hash in the result, it + // should come from a trusted source + let res_height = match res[0..4].try_into() { + Ok(inner) => u32::from_be_bytes(inner), + _ => panic!("Cannot convert result to fixed size array"), + }; + let mut height = self.height.lock().unwrap(); + if let Some(height) = height.as_ref() { + if *height != res_height { + return Err(Error::App(format!( + "Height mismatch: expected {}, got {}", + height, res_height + ))); + } } - } - height.replace(res_height); - let root_hash = match res[4..36].try_into() { - Ok(inner) => inner, - _ => panic!("Cannot convert result to fixed size array"), - }; - let proof_bytes = &res[36..]; + height.replace(res_height); + let root_hash = match res[4..36].try_into() { + Ok(inner) => inner, + _ => panic!("Cannot convert result to fixed size array"), + }; + let proof_bytes = &res[36..]; - let map = nomic::orga::merk::merk::proofs::query::verify(proof_bytes, root_hash)?; + let map = nomic::orga::merk::merk::proofs::query::verify(proof_bytes, root_hash)?; - let store: Shared = Shared::new(ProofStore(map)); - let store = Store::new(BackingStore::ProofMap(store)); + let store: Shared = Shared::new(ProofStore(map)); + let store = Store::new(BackingStore::ProofMap(store)); - Ok(store) + Ok(store) + }) + .await + } +} + +pub struct UnsafeSendFuture(F); + +unsafe impl Send for UnsafeSendFuture {} + +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +impl Future for UnsafeSendFuture { + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let inner = unsafe { self.map_unchecked_mut(|s| &mut s.0) }; + inner.poll(cx) } }