diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index 0d406fe3..adcaf0d5 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + [[package]] name = "arrayvec" version = "0.7.4" @@ -169,6 +184,12 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minreq" version = "2.11.2" @@ -198,6 +219,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ryu" version = "1.0.18" @@ -283,6 +333,14 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "verify" +version = "0.1.0" +dependencies = [ + "anyhow", + "regex", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 0d406fe3..adcaf0d5 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + [[package]] name = "arrayvec" version = "0.7.4" @@ -169,6 +184,12 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minreq" version = "2.11.2" @@ -198,6 +219,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ryu" version = "1.0.18" @@ -283,6 +333,14 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "verify" +version = "0.1.0" +dependencies = [ + "anyhow", + "regex", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 5ec7bc2b..9c329fd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["client", "types", "jsonrpc"] +members = ["client", "types", "jsonrpc", "verify"] exclude = ["integration_test", "node"] resolver = "2" diff --git a/contrib/extract.pl b/contrib/extract.pl new file mode 100755 index 00000000..c7683ee5 --- /dev/null +++ b/contrib/extract.pl @@ -0,0 +1,138 @@ +#!/usr/bin/perl +# SPDX-License-Identifier: CC0-1.0 +# +# Create the `types/src/vXYZ/mod.rs` rusdoc. + +use strict; +use warnings; +use Getopt::Long qw(:config no_auto_abbrev); + +# The script name. +my $SCRIPT = $0; + +# The Bitcoin Core version we are working with. +my $CORE_VERSION = "0.17.1"; +# The file holding output of `bitcoin-cli --help`. +my $RPC_HELP_FILE = "types/src/v17/rpc-api.txt"; + +# Command line options. +my $help = 0; +my $debug = 0; + +sub help +{ + my ($exitcode) = @_; + + print << "EOM"; + +Usage: $SCRIPT [OPTIONS] + +Options: + + -d, --debug Display debugging output. + -h, --help Display this help and exit. + +Generates the rustdocs for the `types/vX.rs` module. Before running script set CORE_VERSION and RPC_HELP_FILE. + +EOM + exit($exitcode); +} + +GetOptions( + 'd|debug' => \$debug, + 'h|help' => \$help, +) or help(1); + +help(0) if ($help); + +main(); + +exit 0; + +sub dprint +{ + printf(STDERR @_) if $debug; +} + +sub main { + + # Open the file for reading + open(my $fh, '<', $RPC_HELP_FILE) or die "Could not open file '$RPC_HELP_FILE': $!\n"; + + # Loop over each line in the file + while (my $line = <$fh>) { + chomp($line); + if ($line =~ /^== (.+) ==/) { # Section heading. + my $section = $1; # Captures the placeholder text + start_section($section); + } elsif ($line =~ /^\s*$/) { # Blank line. + end_section(); + } else { + add_method($line); + } + } + + end_section(); + print_footer(); + + # Close the file handle. + close($fh); +} + +sub print_header { + print <<'EOM'; +//! # JSON-RPC types for Bitcoin Core `v$CORE_VERSION` +//! +//! These structs are shaped for the JSON data returned by the JSON-RPC API. They use stdlib types +//! (or custom types) and where necessary implement an `into_model` function to convert the type to +//! a [`crate::model`] type of the same name. The types in this module are version specific. The +//! types in the `model` module are version nonspecific and are strongly typed using `rust-bitcoin`. +//! +//! ### Method name and implementation status +//! +//! Every JSON-RPC method supported by this version of Bitcoin Core is listed below along with its +//! current implementation status. +//! +EOM +} + +sub print_footer { + print <<'EOM' +//! +//! **Items marked omitted were omitted because:** +//! +//! - Method does not return anything. +//! - Method returns a simple type (e.g. bool or integer). +//! - Method is deprecated. +EOM +} + +# Print the blurb for the start of a section. +sub start_section { + my ($section) = @_; # Get the first argument + + print <<'EOM'; +//!
+//! Methods from the $section section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +EOM +} + +# Print the blurb for the end of a section. +sub end_section { + print<<'EOM'; +//! +//!
+//! +EOM +} + +sub add_method { + my ($line) = @_; + if ($line =~ /^(\S+)/) { + my ($method) = $1; + printf "//! \| %-34s \| |\n", $method + } +} diff --git a/integration_test/tests/blockchain.rs b/integration_test/tests/blockchain.rs index e37b6d88..194bb71d 100644 --- a/integration_test/tests/blockchain.rs +++ b/integration_test/tests/blockchain.rs @@ -76,15 +76,17 @@ fn get_block_header_verbose() { // verbose = true // optional as suggested in the docs but to no avail. #[test] #[cfg(feature = "0_17_1")] +fn get_block_stats() { + get_block_stats_by_height(); + get_block_stats_by_hash(); +} + fn get_block_stats_by_height() { let node = Node::new_no_wallet(); let json = node.client.get_block_stats_by_height(0).expect("getblockstats"); assert!(json.into_model().is_ok()); } -// FIXME: Same as get_block_stats_by_height above. -#[test] -#[cfg(feature = "0_17_1")] fn get_block_stats_by_hash() { // verbose = true let node = Node::new_no_wallet(); let block_hash = best_block_hash(); @@ -92,6 +94,7 @@ fn get_block_stats_by_hash() { // verbose = true assert!(json.into_model().is_ok()); } + #[test] fn get_block_stats_by_height_txindex() { let node = Node::new_no_wallet_txindex(); @@ -130,37 +133,37 @@ fn get_difficulty() { #[test] #[cfg(feature = "TODO")] -fn get_mempool_ancestors() {} +fn get_mempool_ancestors() { todo!() } #[test] #[cfg(feature = "TODO")] -fn get_mempool_descendants() {} +fn get_mempool_descendants() { todo!() } #[test] #[cfg(feature = "TODO")] -fn get_mempool_entry() {} +fn get_mempool_entry() { todo!() } #[test] #[cfg(feature = "TODO")] -fn get_mempool_info() {} +fn get_mempool_info() { todo!() } #[test] #[cfg(feature = "TODO")] -fn get_raw_mempool() {} +fn get_raw_mempool() { todo!() } #[test] #[cfg(feature = "TODO")] -fn get_tx_out() {} +fn get_tx_out() { todo!() } #[test] #[cfg(feature = "TODO")] -fn get_tx_out_proof() {} +fn get_tx_out_proof() { todo!() } #[test] #[cfg(feature = "TODO")] -fn get_tx_out_set_info() {} +fn get_tx_out_set_info() { todo!() } #[test] #[cfg(feature = "TODO")] -fn verify_tx_out_proof() {} +fn verify_tx_out_proof() { todo!() } diff --git a/types/src/v17/mod.rs b/types/src/v17/mod.rs index 53bf0ce7..76281a06 100644 --- a/types/src/v17/mod.rs +++ b/types/src/v17/mod.rs @@ -184,7 +184,7 @@ //! | lockunspent | omitted | //! | move | omitted | //! | removeprunedfunds | omitted | -//! | rescanblockchain | done | +//! | rescanblockchain | done (untested) | //! | sendfrom | omitted | //! | sendmany | done (untested) | //! | sendtoaddress | done | @@ -207,7 +207,7 @@ //! //! | JSON-PRC Method Name | Status | //! |:-----------------------------------|:---------------:| -//! | getzmqnotifications` | done (untested) | +//! | getzmqnotifications | done (untested) | //! //! //! @@ -260,4 +260,5 @@ pub use self::{ SignErrorData, SignMessage, SignRawTransactionWithWallet, TransactionCategory, WalletCreateFundedPsbt, WalletProcessPsbt, }, + zmq::GetZmqNotifications, }; diff --git a/types/src/v18/mod.rs b/types/src/v18/mod.rs index 9efbc10a..890ee956 100644 --- a/types/src/v18/mod.rs +++ b/types/src/v18/mod.rs @@ -8,168 +8,225 @@ // differences. // - Double check that nothing marked `x` has a feature gated test (i.e. ensure tested). -//! Structs with standard types. -//! -//! These structs model the types returned by the JSON-RPC API and use stdlib types (or custom -//! types) and are specific to a specific to Bitcoin Core `v0.18.1`. -//! -//! ## Key: -//! -//! - `[ ]` Not yet done. -//! - `[i]` Implemented but not yet tested (includes `into_model`). -//! - `[x]` Implemented _and_ tested. -//! - `[-]` Intentionally not done, typically for one of the following reasons: -//! - Method does not return anything. -//! - Method returns a simple type (e.g. bool or integer). -//! - Method is deprecated. -// TODO: After all the `[i]` is gone (ie testing done) remove the backticks. -//! -//! ** == Blockchain ==** -//! - `[x]` `getbestblockhash` -//! - `[x]` `getblock "blockhash" ( verbosity )` -//! - `[x]` `getblockchaininfo` -//! - `[x]` `getblockcount` -//! - `[x]` `getblockhash height` -//! - `[x]` `getblockheader "blockhash" ( verbose )` -//! - `[x]` `getblockstats hash_or_height ( stats )` -//! - `[x]` `getchaintips` -//! - `[x]` `getchaintxstats ( nblocks "blockhash" )` -//! - `[x]` `getdifficulty` -//! - `[i]` `getmempoolancestors "txid" ( verbose )` -//! - `[i]` `getmempooldescendants "txid" ( verbose )` -//! - `[i]` `getmempoolentry "txid"` -//! - `[i]` `getmempoolinfo` -//! - `[i]` `getrawmempool ( verbose )` -//! - `[i]` `gettxout "txid" n ( include_mempool )` -//! - `[i]` `gettxoutproof ["txid",...] ( "blockhash" )` -//! - `[i]` `gettxoutsetinfo` -//! - `[-]` `preciousblock "blockhash"` -//! - `[-]` `pruneblockchain height` -//! - `[-]` `savemempool` -//! - `[-]` `scantxoutset "action" [scanobjects,...]` -//! - `[-]` `verifychain ( checklevel nblocks )` -//! - `[i]` `verifytxoutproof "proof"` -//! -//! ** == Control ==** -//! - `[i]` `getmemoryinfo ( "mode" )` -//! - `[ ]` `getrpcinfo` -//! - `[-]` `help ( "command" )` -//! - `[x]` `logging ( ["include_category",...] ["exclude_category",...] )` -//! - `[x]` `stop` -//! - `[x]` `uptime` -//! -//! ** == Generating ==** -//! - `[x]` `generate nblocks ( maxtries )` -//! - `[x]` `generatetoaddress nblocks "address" ( maxtries )` -//! -//! ** == Mining ==** -//! - `[ ]` `getblocktemplate "template_request"` -//! - `[ ]` `getmininginfo` -//! - `[ ]` `getnetworkhashps ( nblocks height )` -//! - `[ ]` `prioritisetransaction "txid" ( dummy ) fee_delta` -//! - `[ ]` `submitblock "hexdata" ( "dummy" )` -//! - `[ ]` `submitheader "hexdata"` -//! -//! ** == Network ==** -//! - `[-]` `addnode "node" "command"` -//! - `[-]` `clearbanned` -//! - `[-]` `disconnectnode ( "address" nodeid )` -//! - `[x]` `getaddednodeinfo ( "node" )` -//! - `[-]` `getconnectioncount` -//! - `[x]` `getnettotals` -//! - `[x]` `getnetworkinfo` -//! - `[ ]` `getnodeaddresses ( count )` -//! - `[x]` `getpeerinfo` -//! - `[-]` `listbanned` -//! - `[-]` `ping` -//! - `[-]` `setban "subnet" "command" ( bantime absolute )` -//! - `[-]` `setnetworkactive state` -//! -//! ** == Rawtransactions ==** -//! - `[ ]` `analyzepsbt "psbt"` -//! - `[ ]` `combinepsbt ["psbt",...]` -//! - `[ ]` `combinerawtransaction ["hexstring",...]` -//! - `[ ]` `converttopsbt "hexstring" ( permitsigdata iswitness )` -//! - `[ ]` `createpsbt [{"txid":"hex","vout":n,"sequence":n},...] [{"address":amount},{"data":"hex"},...] ( locktime replaceable )` -//! - `[ ]` `createrawtransaction [{"txid":"hex","vout":n,"sequence":n},...] [{"address":amount},{"data":"hex"},...] ( locktime replaceable )` -//! - `[ ]` `decodepsbt "psbt"` -//! - `[ ]` `decoderawtransaction "hexstring" ( iswitness )` -//! - `[ ]` `decodescript "hexstring"` -//! - `[ ]` `finalizepsbt "psbt" ( extract )` -//! - `[ ]` `fundrawtransaction "hexstring" ( options iswitness )` -//! - `[ ]` `getrawtransaction "txid" ( verbose "blockhash" )` -//! - `[ ]` `joinpsbts ["psbt",...]` -//! - `[i]` `sendrawtransaction "hexstring" ( allowhighfees )` -//! - `[ ]` `signrawtransactionwithkey "hexstring" ["privatekey",...] ( [{"txid":"hex","vout":n,"scriptPubKey":"hex","redeemScript":"hex","witnessScript":"hex","amount":amount},...] "sighashtype" )` -//! - `[ ]` `testmempoolaccept ["rawtx",...] ( allowhighfees )` -//! - `[ ]` `utxoupdatepsbt "psbt"` -//! -//! ** == Util ==** -//! - `[ ]` `createmultisig nrequired ["key",...] ( "address_type" )` -//! - `[ ]` `deriveaddresses "descriptor" ( range )` -//! - `[ ]` `estimatesmartfee conf_target ( "estimate_mode" )` -//! - `[ ]` `getdescriptorinfo "descriptor"` -//! - `[ ]` `signmessagewithprivkey "privkey" "message"` -//! - `[ ]` `validateaddress "address"` -//! - `[ ]` `verifymessage "address" "signature" "message"` -//! -//! ** == Wallet ==** -//! - `[-]` `abandontransaction "txid"` -//! - `[-]` `abortrescan` -//! - `[x]` `addmultisigaddress nrequired ["key",...] ( "label" "address_type" )` -//! - `[-]` `backupwallet "destination"` -//! - `[x]` `bumpfee "txid" ( options )` -//! - `[x]` `createwallet "wallet_name" ( disable_private_keys blank )` -//! - `[x]` `dumpprivkey "address"` -//! - `[x]` `dumpwallet "filename"` -//! - `[-]` `encryptwallet "passphrase"` -//! - `[x]` `getaddressesbylabel "label"` -//! - `[x]` `getaddressinfo "address"` -//! - `[x]` `getbalance ( "dummy" minconf include_watchonly )` -//! - `[x]` `getnewaddress ( "label" "address_type" )` -//! - `[x]` `getrawchangeaddress ( "address_type" )` -//! - `[i]` `getreceivedbyaddress "address" ( minconf )` -//! - `[ ]` `getreceivedbylabel "label" ( minconf )` -//! - `[x]` `gettransaction "txid" ( include_watchonly )` -//! - `[i]` `getunconfirmedbalance` -//! - `[i]` `getwalletinfo` -//! - `[-]` `importaddress "address" ( "label" rescan p2sh )` -//! - `[-]` `importmulti "requests" ( "options" )` -//! - `[-]` `importprivkey "privkey" ( "label" rescan )` -//! - `[-]` `importprunedfunds "rawtransaction" "txoutproof"` -//! - `[-]` `importpubkey "pubkey" ( "label" rescan )` -//! - `[-]` `importwallet "filename"` -//! - `[-]` `keypoolrefill ( newsize )` -//! - `[i]` `listaddressgroupings` -//! - `[i]` `listlabels ( "purpose" )` -//! - `[i]` `listlockunspent` -//! - `[i]` `listreceivedbyaddress ( minconf include_empty include_watchonly "address_filter" )` -//! - `[ ]` `listreceivedbylabel ( minconf include_empty include_watchonly )` -//! - `[i]` `listsinceblock ( "blockhash" target_confirmations include_watchonly include_removed )` -//! - `[i]` `listtransactions ( "label" count skip include_watchonly )` -//! - `[i]` `listunspent ( minconf maxconf ["address",...] include_unsafe query_options )` -//! - `[ ]` `listwalletdir` -//! - `[i]` `listwallets` -//! - `[x]` `loadwallet "filename"` -//! - `[-]` `lockunspent unlock ( [{"txid":"hex","vout":n},...] )` -//! - `[i]` `removeprunedfunds "txid"` -//! - `[x]` `rescanblockchain ( start_height stop_height )` -//! - `[i]` `sendmany "" {"address":amount} ( minconf "comment" ["address",...] replaceable conf_target "estimate_mode" )` -//! - `[x]` `sendtoaddress "address" amount ( "comment" "comment_to" subtractfeefromamount replaceable conf_target "estimate_mode" )` -//! - `[i]` `sethdseed ( newkeypool "seed" )` -//! - `[ ]` `setlabel "address" "label"` -//! - `[ ]` `settxfee amount` -//! - `[i]` `signmessage "address" "message"` -//! - `[i]` `signrawtransactionwithwallet "hexstring" ( [{"txid":"hex","vout":n,"scriptPubKey":"hex","redeemScript":"hex","witnessScript":"hex","amount":amount},...] "sighashtype" )` -//! - `[-]` `unloadwallet ( "wallet_name" )` -//! - `[i]` `walletcreatefundedpsbt [{"txid":"hex","vout":n,"sequence":n},...] [{"address":amount},{"data":"hex"},...] ( locktime options bip32derivs )` -//! - `[-]` `walletlock` -//! - `[-]` `walletpassphrase "passphrase" timeout` -//! - `[-]` `walletpassphrasechange "oldpassphrase" "newpassphrase"` -//! - `[-]` `walletprocesspsbt "psbt" ( sign "sighashtype" bip32derivs )` -//! -//! ** == Zmq ==**` -//! - `[i]` `getzmqnotifications` +//! # JSON-RPC types for Bitcoin Core `v0.18.1` +//! +//! These structs are shaped for the JSON data returned by the JSON-RPC API. They use stdlib types +//! (or custom types) and where necessary implement an `into_model` function to convert the type to +//! a [`crate::model`] type of the same name. The types in this module are version specific. The +//! types in the `model` module are version nonspecific and are strongly typed using `rust-bitcoin`. +//! +//! ### Method name and implementation status +//! +//! Every JSON-RPC method supported by this version of Bitcoin Core is listed below along with its +//! current implementation status. +//! +//!
+//! Methods from the == Blockchain == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | getbestblockhash | done | +//! | getblock | done | +//! | getblockchaininfo | done | +//! | getblockcount | done | +//! | getblockhash | done | +//! | getblockheader | done | +//! | getblockstats | done | +//! | getchaintips | done | +//! | getchaintxstats | done | +//! | getdifficulty | done | +//! | getmempoolancestors | done (untested) | +//! | getmempooldescendants | done (untested) | +//! | getmempoolentry | done (untested) | +//! | getmempoolinfo | done (untested) | +//! | getrawmempool | done (untested) | +//! | gettxout | done (untested) | +//! | gettxoutproof | done (untested) | +//! | gettxoutsetinfo | done (untested) | +//! | preciousblock | omitted | +//! | pruneblockchain | omitted | +//! | savemempool | omitted | +//! | scantxoutset | omitted | +//! | verifychain | omitted | +//! | verifytxoutproof | done (untested) | +//! +//!
+//! +//!
+//! Methods from the == Control == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | getmemoryinfo | done | +//! | getrpcinfo | todo | +//! | help | omitted | +//! | logging | done | +//! | stop | omitted | +//! | uptime | omitted | +//! +//!
+//! +//!
+//! Methods from the == Generating == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | generate | done | +//! | generatetoaddress | done | +//! +//!
+//! +//!
+//! Methods from the == Mining == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | getblocktemplate | todo | +//! | getmininginfo | todo | +//! | getnetworkhashps | todo | +//! | prioritisetransaction | todo | +//! | submitblock | todo | +//! | submitheader | todo | +//! +//!
+//! +//!
+//! Methods from the == Network == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | addnode | omitted | +//! | clearbanned | omitted | +//! | disconnectnode | omitted | +//! | getaddednodeinfo | done | +//! | getconnectioncount | omitted | +//! | getnettotals | done | +//! | getnetworkinfo | done | +//! | getnodeaddresses | todo | +//! | getpeerinfo | done | +//! | listbanned | omitted | +//! | ping | omitted | +//! | setban | omitted | +//! | setnetworkactive | omitted | +//! +//!
+//! +//!
+//! Methods from the == Rawtransactions == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | analyzepsbt | todo | +//! | combinepsbt | todo | +//! | combinerawtransaction | todo | +//! | converttopsbt | todo | +//! | createpsbt | todo | +//! | createrawtransaction | todo | +//! | decodepsbt | todo | +//! | decoderawtransaction | todo | +//! | decodescript | todo | +//! | finalizepsbt | todo | +//! | fundrawtransaction | todo | +//! | getrawtransaction | todo | +//! | joinpsbts | todo | +//! | sendrawtransaction | done | +//! | signrawtransactionwithkey | todo | +//! | testmempoolaccept | todo | +//! | utxoupdatepsbt | todo | +//! +//!
+//! +//!
+//! Methods from the == Util == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | createmultisig | omitted | +//! | deriveaddresses | todo | +//! | estimatesmartfee | omitted | +//! | getdescriptorinfo | todo | +//! | signmessagewithprivkey | omitted | +//! | validateaddress | omitted | +//! | verifymessage | omitted | +//! +//!
+//! +//!
+//! Methods from the == Wallet == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | abandontransaction | omitted | +//! | abortrescan | omitted | +//! | addmultisigaddress | done | +//! | backupwallet | omitted | +//! | bumpfee | done | +//! | createwallet | done | +//! | dumpprivkey | done | +//! | dumpwallet | done | +//! | encryptwallet | omitted | +//! | getaddressesbylabel | done | +//! | getaddressinfo | done | +//! | getbalance | done | +//! | getnewaddress | done | +//! | getrawchangeaddress | done | +//! | getreceivedbyaddress | done | +//! | getreceivedbylabel | todo | +//! | gettransaction | done | +//! | getunconfirmedbalance | done (untested) | +//! | getwalletinfo | done (untested) | +//! | importaddress | omitted | +//! | importmulti | omitted | +//! | importprivkey | omitted | +//! | importprunedfunds | omitted | +//! | importpubkey | omitted | +//! | importwallet | omitted | +//! | keypoolrefill | omitted | +//! | listaddressgroupings | done (untested) | +//! | listlabels | done (untested) | +//! | listlockunspent | done (untested) | +//! | listreceivedbyaddress | done (untested) | +//! | listreceivedbylabel | todo | +//! | listsinceblock | done (untested) | +//! | listtransactions | done (untested) | +//! | listunspent | done (untested) | +//! | listwalletdir | todo | +//! | listwallets | done (untested) | +//! | loadwallet | done | +//! | lockunspent | omitted | +//! | removeprunedfunds | omitted | +//! | rescanblockchain | done (untested) | +//! | sendmany | done (untested) | +//! | sendtoaddress | done | +//! | sethdseed | omitted | +//! | setlabel | todo | +//! | settxfee | omitted | +//! | signmessage | done (untested) | +//! | signrawtransactionwithwallet | done (untested) | +//! | unloadwallet | omitted | +//! | walletcreatefundedpsbt | done (untested) | +//! | walletlock | omitted | +//! | walletpassphrase | omitted | +//! | walletpassphrasechange | omitted | +//! | walletprocesspsbt | done (untested) | +//! +//!
+//! +//!
+//! Methods from the == Zmq == section +//! +//! | JSON-PRC Method Name | Status | +//! |:-----------------------------------|:---------------:| +//! | getzmqnotifications | done (untested) | +//! +//!
+//! +//! +//! **Items marked omitted were omitted because:** +//! +//! - Method does not return anything. +//! - Method returns a simple type (e.g. bool or integer). +//! - Method is deprecated. #[doc(inline)] pub use crate::v17::{ @@ -184,12 +241,12 @@ pub use crate::v17::{ GetNetworkInfoAddress, GetNetworkInfoError, GetNetworkInfoNetwork, GetNewAddress, GetPeerInfo, GetRawChangeAddress, GetRawMempool, GetRawMempoolVerbose, GetReceivedByAddress, GetTransaction, GetTransactionDetail, GetTxOut, GetTxOutProof, GetTxOutSetInfo, GetUnconfirmedBalance, - GetWalletInfo, ListAddressGroupings, ListAddressGroupingsItem, ListBanned, ListLabels, - ListLockUnspent, ListLockUnspentItem, ListReceivedByAddress, ListReceivedByAddressItem, - ListSinceBlock, ListSinceBlockTransaction, ListTransactions, ListTransactionsItem, ListUnspent, - ListUnspentItem, ListWallets, LoadWallet, Locked, Logging, MempoolEntry, MempoolEntryFees, - PeerInfo, RescanBlockchain, ScriptPubkey, SendMany, SendRawTransaction, SendToAddress, - SignErrorData, SignMessage, SignRawTransactionWithWallet, Softfork, SoftforkReject, - TransactionCategory, UploadTarget, Uptime, VerifyTxOutProof, WalletCreateFundedPsbt, - WalletProcessPsbt, + GetWalletInfo, GetZmqNotifications, ListAddressGroupings, ListAddressGroupingsItem, ListBanned, + ListLabels, ListLockUnspent, ListLockUnspentItem, ListReceivedByAddress, + ListReceivedByAddressItem, ListSinceBlock, ListSinceBlockTransaction, ListTransactions, + ListTransactionsItem, ListUnspent, ListUnspentItem, ListWallets, LoadWallet, Locked, Logging, + MempoolEntry, MempoolEntryFees, PeerInfo, RescanBlockchain, ScriptPubkey, SendMany, + SendRawTransaction, SendToAddress, SignErrorData, SignMessage, SignRawTransactionWithWallet, + Softfork, SoftforkReject, TransactionCategory, UploadTarget, Uptime, VerifyTxOutProof, + WalletCreateFundedPsbt, WalletProcessPsbt, }; diff --git a/verify/Cargo.toml b/verify/Cargo.toml new file mode 100644 index 00000000..573376d4 --- /dev/null +++ b/verify/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "verify" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.93" +regex = "1" diff --git a/types/src/v17/rpc-api.txt b/verify/rpc-api-v17.txt similarity index 99% rename from types/src/v17/rpc-api.txt rename to verify/rpc-api-v17.txt index 7b5d2f88..2001348f 100644 --- a/types/src/v17/rpc-api.txt +++ b/verify/rpc-api-v17.txt @@ -1,6 +1,6 @@ == Blockchain == getbestblockhash -getblock "blockhash" ( verbosity ) +getblock "blockhash" ( verbosity ) getblockchaininfo getblockcount getblockhash height diff --git a/types/src/v18/rpc-api.txt b/verify/rpc-api-v18.txt similarity index 100% rename from types/src/v18/rpc-api.txt rename to verify/rpc-api-v18.txt diff --git a/verify/src/lib.rs b/verify/src/lib.rs new file mode 100644 index 00000000..af427bd4 --- /dev/null +++ b/verify/src/lib.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Helper methods. + +pub mod method; +pub mod model; +pub mod ssot; +pub mod versioned; + +use std::fmt; +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::Path; +use std::str::FromStr; + +use anyhow::{Context, Result}; +use regex::Regex; + +/// Supported Bitcoin Core versions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Version { + /// Bitcoin Core v17. + V17, + /// Bitcoin Core v18. + V18, +} + +impl Version { + /// Creates a new `Version` from string. + pub fn new(v: &str) -> Result { + match v { + "v17" | "17" => Ok(Version::V17), + "v18" | "18" => Ok(Version::V18), + other => Err(anyhow::Error::msg(format!("unknown version: '{}'", other))), + } + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Version::*; + let s = match *self { + V17 => "v17", + V18 => "v18", + }; + fmt::Display::fmt(&s, f) + } +} + +impl FromStr for Version { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { Version::new(s) } +} + +/// Checks that `got` contains all methods from `want` and no additional methods. +/// +/// # Returns +/// +/// `true` if all methods are correct, `false` otherwise. +pub fn correct_methods(got: &[&str], want: &[&str], msg: &str) -> bool { + let mut correct = true; + let ret = has_all_expected(got, want); + if !ret.is_empty() { + eprintln!("\nMissing methods ({}):", msg); + correct = false; + for method in ret { + eprintln!(" - {}", method); + } + eprintln!(); + } + + let ret = has_no_additional(got, want); + if !ret.is_empty() { + eprintln!("Unexpected additional methods ({}):", msg); + correct = false; + for method in ret { + eprintln!(" - {}", method); + } + eprintln!(); + } + correct +} + +/// Checks that all methods in `want` exist in `got`. +/// +/// # Returns +/// +/// A list of any methods found to be missing. +fn has_all_expected<'b>(got: &[&str], want: &[&'b str]) -> Vec<&'b str> { + let mut missing = vec![]; + for method in want { + if !got.contains(method) { + missing.push(*method); + } + } + missing +} + +/// Checks that no methods in `got` exist in `want`. +/// +/// # Returns +/// +/// A list of any methods found to exist when they should not. +fn has_no_additional<'a>(got: &[&'a str], want: &[&str]) -> Vec<&'a str> { + let mut additional = vec![]; + // We did not get any additional methods we didn't expect. + for method in got { + if !want.contains(method) { + additional.push(*method); + } + } + additional +} + +/// Opens file at `path` and greps for `s`. +pub fn grep_for_string(path: &Path, s: &str) -> Result { + let file = File::open(path) + .with_context(|| format!("Failed to grep for string in {}", path.display()))?; + let reader = io::BufReader::new(file); + + let re = Regex::new(s)?; + + for line in reader.lines() { + let line = line?; + + if re.is_match(&line) { + return Ok(true); + } + } + Ok(false) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn equal() { + let got = vec!["one", "two", "three"]; + let want = vec!["one", "two", "three"]; + + assert!(has_all_expected(&got, &want).is_empty()); + assert!(has_no_additional(&got, &want).is_empty()); + } + + #[test] + fn missing_from_beginning() { + let got = vec!["two", "three"]; + let want = vec!["one", "two", "three"]; + assert_eq!(has_all_expected(&got, &want), &["one"]); + } + + #[test] + fn missing_from_middle() { + let got = vec!["one", "three"]; + let want = vec!["one", "two", "three"]; + assert_eq!(has_all_expected(&got, &want), &["two"]); + } + + #[test] + fn missing_from_end() { + let got = vec!["one", "two"]; + let want = vec!["one", "two", "three"]; + assert_eq!(has_all_expected(&got, &want), &["three"]); + } + + #[test] + fn has_additional_at_beginning() { + let got = vec!["one", "two", "three"]; + let want = vec!["two", "three"]; + + assert_eq!(has_no_additional(&got, &want), &["one"]); + } + + #[test] + fn has_additional_in_middle() { + let got = vec!["one", "two", "three"]; + let want = vec!["one", "three"]; + + assert_eq!(has_no_additional(&got, &want), &["two"]); + } + + #[test] + fn has_additional_at_end() { + let got = vec!["one", "two", "three"]; + let want = vec!["one", "two"]; + + assert_eq!(has_no_additional(&got, &want), &["three"]); + } +} diff --git a/verify/src/main.rs b/verify/src/main.rs new file mode 100644 index 00000000..765f5c0e --- /dev/null +++ b/verify/src/main.rs @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Tool to help verify that what we claim in the rustdocs is true. +//! +//! Currently verifies: +//! +//! - That the correct set of methods is documented. +//! - That an expected return type is provided if the method is supported. +//! - That there is a `model` type if required. +//! - That the method has an integration test. + +use std::env; + +use anyhow::Result; +use verify::method::{Method, Return}; +use verify::versioned::{self, Status}; +use verify::{method, model, ssot, Version}; + +/// The default version of Core if no command line argument is provided. +const DEFAULT_VERSION: Version = Version::V17; + +// FIXME: Enable running from any directory, currently errors if run from `src/`. + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + + let version = if args.len() < 2 { + DEFAULT_VERSION + } else { + let v = &args[1]; + v.parse::()? + }; + println!("\nVerifying support for Bitcoin Core {}\n", version); + + let s = format!("{}::METHOD data", version); + let msg = format!("Checking that the {} list is correct", s); + check(&msg); + let correct = verify_correct_methods(version, method::all_methods(version), &s)?; + close(correct); + if !correct { + std::process::exit(1); + } + + let s = "rustdoc version specific rustdocs"; + let msg = format!("Checking that the {} list is correct", s); + check(&msg); + let correct = verify_correct_methods(version, method::all_methods(version), s)?; + close(correct); + if !correct { + std::process::exit(1); + } + + let msg = "Checking that the status claimed in the version specific rustdocs is correct"; + check(msg); + verify_status(version)?; + close(correct); + + Ok(()) +} + +fn check(msg: &str) { println!("{} ... ", msg) } + +fn close(correct: bool) { + if correct { + println!("Correct \u{2713} \n"); + } +} + +/// Verifies that the correct set of methods are documented. +fn verify_correct_methods(version: Version, methods: Vec, msg: &str) -> Result { + let ssot = ssot::all_methods(version)?; + let want = ssot.iter().map(|s| s.as_str()).collect::>(); + + let got = methods.iter().map(|s| s.as_str()).collect::>(); + + Ok(verify::correct_methods(&got, &want, msg)) +} + +/// Verifies that the status we claim is correct. +fn verify_status(version: Version) -> Result<()> { + let methods = versioned::methods_and_status(version)?; + for method in methods { + let out = + Method::from_name(version, &method.name).expect("guaranteed by methods_and_status()"); + match method.status { + Status::Done => { + if !versioned::return_type_exists(version, &method.name)? { + eprintln!("missing return type: {}", output_method(out)); + } + if !model::type_exists(version, &method.name)? { + eprintln!("missing model type: {}", output_method(out)); + } + if !check_integration_test_crate::test_exists(version, &method.name)? { + eprintln!("missing integration test: {}", method.name); + } + } + Status::Untested => { + if !versioned::return_type_exists(version, &method.name)? { + eprintln!("missing return type: {}", output_method(out)); + } + if !model::type_exists(version, &method.name)? { + eprintln!("missing model type: {}", output_method(out)); + } + // Make sure we didn't forget to mark as tested after implementing integration test. + if check_integration_test_crate::test_exists(version, &method.name)? { + eprintln!("found integration test for untested method: {}", method.name); + } + } + Status::Omitted | Status::Todo => { /* Nothing to verify */ } + } + } + + Ok(()) +} + +fn output_method(method: &Method) -> String { + if let Some(Return::Type(s)) = method.ret { + format!("{} {}", method.name, s) + } else { + method.name.to_string() + } +} + +// Use a module because a file with this name is confusing. +mod check_integration_test_crate { + //! Things related to parsing the `integration_test` crate. + + use std::fs::File; + use std::io::{self, BufRead}; + use std::path::PathBuf; + + use anyhow::{Context, Result}; + use regex::Regex; + use verify::method; + + use crate::Version; + + /// Path to the model module file. + fn paths() -> Vec { + // TODO: "mining", "util", "zmq" + let sections = + ["blockchain", "control", "generating", "network", "raw_transactions", "wallet"]; + let mut paths = vec![]; + for section in sections { + paths.push(PathBuf::from(format!("../integration_test/tests/{}.rs", section))); + } + paths + } + + fn all_test_functions() -> Result> { + let mut functions = vec![]; + + for path in paths() { + let file = File::open(&path).with_context(|| { + format!("Failed to grep for test functions in {}", path.display()) + })?; + let reader = io::BufReader::new(file); + + // let re = Regex::new(®ex::escape(r"fn ([a-z_]+)\(\) \{"))?; + let fn_re = Regex::new(r"fn ([a-z_]+)")?; + let todo_re = Regex::new(r"todo")?; + + for line in reader.lines() { + let line = line?; + + if todo_re.is_match(&line) { + continue; + } + + if let Some(caps) = fn_re.captures(&line) { + let function = caps.get(1).unwrap().as_str(); + functions.push(function.to_string()); + } + } + } + Ok(functions) + } + + /// Checks that a type exists in `model` module. + pub fn test_exists(version: Version, method_name: &str) -> Result { + let method = match method::Method::from_name(version, method_name) { + Some(m) => m, + None => + return Err(anyhow::Error::msg(format!( + "expected test method not found: {}", + method_name + ))), + }; + + let tests = all_test_functions()?; + if !tests.contains(&method.function.to_string()) { + Ok(false) + } else { + Ok(true) + } + } +} diff --git a/verify/src/method/mod.rs b/verify/src/method/mod.rs new file mode 100644 index 00000000..4dc91e32 --- /dev/null +++ b/verify/src/method/mod.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Provides a data structure that describes each JSON RPC method. + +pub mod v17; +pub mod v18; + +use crate::Version; + +/// Returns a list of all the method names. +pub fn all_methods(version: Version) -> Vec { + use Version::*; + + let list = match version { + V17 => v17::METHODS, + V18 => v18::METHODS, + }; + + list.iter().map(|m| m.name.to_string()).collect() +} + +/// Describes a single JSON RPC method. +// TODO: This name is overloaded (`versioned::Method`). +#[derive(Debug)] +pub struct Method { + /// The method name. + pub name: &'static str, + /// The type it is expected to return (method name in snake case). + /// + /// `None` if the method is either intentionally omitted or not yet done. + pub ret: Option, + /// `true` if this method requires a type to exist in `model`. + /// + /// This is true if the return type includes types that can be + /// more strongly typed using `rust-bitcoin`. + pub requires_model: bool, + /// The function name (snake case) for this method. + pub function: &'static str, +} + +impl Method { + /// Returns a `Method` type if one exists in the `METHODS` list for `name`. + pub fn from_name(version: Version, name: &str) -> Option<&'static Method> { + use Version::*; + let list = match version { + V17 => v17::METHODS, + V18 => v18::METHODS, + }; + + list.iter().find(|&method| method.name == name) + } + + /// Represents a `Method` that requires a custom type as well as a type in `model`. + const fn new_modeled(name: &'static str, ty: &'static str, function: &'static str) -> Method { + Method { name, ret: Some(Return::Type(ty)), requires_model: true, function } + } + + /// Represents a method that requires a custom type as but no type in `model`. + /// + /// Implies this method does not return data that can be strongly type using `rust-bitcoin`. + const fn new_no_model(name: &'static str, ty: &'static str, function: &'static str) -> Method { + Method { name, ret: Some(Return::Type(ty)), requires_model: false, function } + } + + const fn new_nothing(name: &'static str, function: &'static str) -> Method { + Method { name, ret: Some(Return::Nothing), function, requires_model: false } + } + + const fn new_numeric(name: &'static str, function: &'static str) -> Method { + Method { name, ret: Some(Return::Numeric), function, requires_model: false } + } + + const fn new_bool(name: &'static str, function: &'static str) -> Method { + Method { name, ret: Some(Return::Bool), function, requires_model: false } + } + + const fn new_string(name: &'static str, function: &'static str) -> Method { + Method { name, ret: Some(Return::String), function, requires_model: false } + } + + const fn new_none(name: &'static str, function: &'static str) -> Method { + Method { name, ret: None, function, requires_model: false } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Return { + /// Method returns a type (that should exist). + Type(&'static str), + /// Method does not return anything. + Nothing, + /// Method returns a numeric type. + Numeric, + /// Method returns a boolean. + Bool, + /// Method returns a string. + String, +} diff --git a/verify/src/method/v17.rs b/verify/src/method/v17.rs new file mode 100644 index 00000000..1d4db878 --- /dev/null +++ b/verify/src/method/v17.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! JSON RPC methods provided by Bitcoin Core v17. + +use super::Method; + +/// Data for the JSON RPC methods provided by Bitcoin Core v17. +pub const METHODS: &[Method] = &[ + Method::new_modeled("getbestblockhash", "GetBestBlockHash", "get_best_block_hash"), + Method::new_modeled("getblock", "GetBlock", "get_block"), + Method::new_modeled("getblockchaininfo", "GetBlockchainInfo", "get_blockchain_info"), + Method::new_modeled("getblockcount", "GetBlockCount", "get_block_count"), + Method::new_modeled("getblockhash", "GetBlockHash", "get_block_hash"), + Method::new_modeled("getblockheader", "GetBlockHeader", "get_block_header"), + Method::new_modeled("getblockstats", "GetBlockStats", "get_block_stats"), + Method::new_modeled("getchaintips", "GetChainTips", "get_chain_tips"), + Method::new_modeled("getchaintxstats", "GetChainTxStats", "get_chain_tx_stats"), + Method::new_modeled("getdifficulty", "GetDifficulty", "get_difficulty"), + Method::new_modeled("getmempoolancestors", "GetMempoolAncestors", "get_mempool_ancestors"), + Method::new_modeled( + "getmempooldescendants", + "GetMempoolDescendants", + "get_mempool_descendants", + ), + Method::new_modeled("getmempoolentry", "GetMempoolEntry", "get_mempool_entry"), + Method::new_modeled("getmempoolinfo", "GetMempoolInfo", "get_mempool_info"), + Method::new_modeled("getrawmempool", "GetRawMempool", "get_raw_mempool"), + Method::new_modeled("gettxout", "GetTxOut", "get_tx_out"), + Method::new_modeled("gettxoutproof", "GetTxOutProof", "get_tx_out_proof"), + Method::new_modeled("gettxoutsetinfo", "GetTxOutSetInfo", "get_tx_out_set_info"), + Method::new_nothing("preciousblock", "precious_block"), + Method::new_numeric("pruneblockchain", "prune_blockchain"), + Method::new_nothing("savemempool", "save_mempool"), + Method::new_modeled("scantxoutset", "ScanTxOutSet", "scan_tx_out_set"), + Method::new_bool("verifychain", "verify_chain"), + Method::new_modeled("verifytxoutproof", "VerifyTxOutProof", "verify_tx_out_proof"), + Method::new_no_model("getmemoryinfo", "GetMemoryInfo", "get_memory_info"), + Method::new_string("help", "help"), + Method::new_no_model("logging", "Logging", "logging"), + Method::new_nothing("stop", "stop"), + Method::new_numeric("uptime", "uptime"), + Method::new_modeled("generate", "Generate", "generate"), + Method::new_modeled("generatetoaddress", "GenerateToAddress", "generate_to_address"), + Method::new_none("getblocktemplate", "get_block_template"), + Method::new_none("getmininginfo", "get_mining_info"), + Method::new_none("getnetworkhashps", "get_network_hashes_per_second"), + Method::new_bool("prioritisetransaction", "prioritise_transaction"), + Method::new_none("submitblock", "submit_block"), + Method::new_nothing("addnode", "add_node"), + Method::new_nothing("clearbanned", "clear_banned"), + Method::new_nothing("disconnectnode", "disconnect_node"), + Method::new_no_model("getaddednodeinfo", "GetAddedNodeInfo", "get_added_node_info"), + Method::new_numeric("getconnectioncount", "get_connection_count"), + Method::new_no_model("getnettotals", "GetNetTotals", "get_net_totals"), + Method::new_modeled("getnetworkinfo", "GetNetworkInfo", "get_network_info"), + Method::new_no_model("getpeerinfo", "GetPeerInfo", "get_peer_info"), + Method::new_string("listbanned", "list_banned"), // v17 docs seem wrong, says no return. + Method::new_nothing("ping", "ping"), + Method::new_nothing("setban", "set_ban"), + Method::new_nothing("setnetworkactive", "set_network_active"), + Method::new_none("combinepsbt", "combine_psbt"), + Method::new_none("combinerawtransaction", "combine_raw_transaction"), + Method::new_none("converttopsbt", "convert_to_psbt"), + Method::new_none("createpsbt", "create_psbt"), + Method::new_none("createrawtransaction", "create_raw_transaction"), + Method::new_none("decodepsbt", "decode_psbt"), + Method::new_none("decoderawtransaction", "decode_raw_transaction"), + Method::new_none("decodescript", "decode_script"), + Method::new_none("finalizepsbt", "finalize_psbt"), + Method::new_none("fundrawtransaction", "fund_raw_transaciton"), + Method::new_none("getrawtransaction", "get_raw_transaction"), + Method::new_modeled("sendrawtransaction", "SendRawTransaction", "send_raw_transaction"), + Method::new_none("signrawtransaction", "sign_raw_transaction"), + Method::new_none("signrawtransactionwithkey", "sign_raw_transaction_with_key"), + Method::new_none("testmempoolaccept", "test_mempool_accept"), + Method::new_modeled("createmultisig", "CreateMultisig", "create_multisig"), + Method::new_nothing("estimatesmartfee", "estimate_smart_fee"), + Method::new_string("signmessagewithprivkey", "sign_message_with_priv_key"), + Method::new_modeled("validateaddress", "ValidateAddress", "validate_address"), + Method::new_bool("verifymessage", "verify_message"), + Method::new_nothing("abandontransaction", "abandon_transaction"), + Method::new_nothing("abortrescan", "abort_rescan"), + Method::new_modeled("addmultisigaddress", "AddMultisigAddress", "add_multisig_address"), + Method::new_nothing("backupwallet", "backup_wallet"), + Method::new_modeled("bumpfee", "BumpFee", "bump_fee"), + Method::new_modeled("createwallet", "CreateWallet", "create_wallet"), + Method::new_modeled("dumpprivkey", "DumpPrivKey", "dump_priv_key"), + Method::new_modeled("dumpwallet", "DumpWallet", "dump_wallet"), + Method::new_nothing("encryptwallet", "encrypt_wallet"), + Method::new_none("getaccount", "get_account"), // Deprecated + Method::new_none("getaccountaddress", "get_account_address"), // Deprecated + Method::new_none("getaddressbyaccount", "get_address_by_account"), // Deprecated + Method::new_modeled("getaddressesbylabel", "GetAddressesByLabel", "get_addresses_by_label"), + Method::new_modeled("getaddressinfo", "GetAddressInfo", "get_address_info"), + Method::new_modeled("getbalance", "GetBalance", "get_balance"), + Method::new_modeled("getnewaddress", "GetNewAddress", "get_new_address"), + Method::new_modeled("getrawchangeaddress", "GetRawChangeAddress", "get_raw_change_address"), + Method::new_none("getreceivedbyaccount", "get_received_by_account"), // Deprecated + Method::new_modeled("getreceivedbyaddress", "GetReceivedByAddress", "get_received_by_address"), + Method::new_modeled("gettransaction", "GetTransaction", "get_transaction"), + Method::new_modeled( + "getunconfirmedbalance", + "GetUnconfirmedBalance", + "get_unconfirmed_balance", + ), + Method::new_modeled("getwalletinfo", "GetWalletInfo", "get_wallet_info"), + Method::new_nothing("importaddress", "import_addressss"), + Method::new_nothing("importmulti", "import_multi"), + Method::new_nothing("importprivkey", "import_priv_key"), + Method::new_nothing("importprunedfunds", "import_pruned_funds"), + Method::new_nothing("importpubkey", "import_pubkey"), + Method::new_nothing("importwallet", "import_walet"), + Method::new_nothing("keypoolrefill", "keypool_refill"), + Method::new_none("listaccounts", "list_accounts"), // Deprecated + Method::new_modeled("listaddressgroupings", "ListAddressGroupings", "list_address_groupings"), + Method::new_modeled("listlabels", "ListLabels", "list_labels"), + Method::new_modeled("listlockunspent", "ListLockUnspent", "list_lock_unspent"), + Method::new_none("listreceivedbyaccount", "list_received_by_account"), // Deprecated + Method::new_modeled( + "listreceivedbyaddress", + "ListReceivedByAddress", + "list_received_by_address", + ), + Method::new_modeled("listsinceblock", "ListSinceBlock", "list_since_block"), + Method::new_modeled("listtransactions", "ListTransactions", "list_transactions"), + Method::new_modeled("listunspent", "ListUnspent", "list_unspent"), + Method::new_modeled("listwallets", "ListWallets", "list_wallets"), + Method::new_modeled("loadwallet", "LoadWallet", "load_wallet"), + Method::new_bool("lockunspent", "lock_unspent"), + Method::new_bool("move", "move"), + Method::new_nothing("removeprunedfunds", "remove_pruned_funds"), + Method::new_modeled("rescanblockchain", "RescanBlockchain", "rescan_blockchain"), + Method::new_none("sendfrom", "send_from"), // Deprecated + Method::new_modeled("sendmany", "SendMany", "send_many"), + Method::new_modeled("sendtoaddress", "SendToAddress", "send_to_address"), + Method::new_none("setaccount", "set_account"), // Deprecated + Method::new_nothing("sethdseed", "set_hd_seed"), + Method::new_bool("settxfee", "set_tx_fee"), + Method::new_modeled("signmessage", "SignMessage", "sign_message"), + Method::new_modeled( + "signrawtransactionwithwallet", + "SignRawTransactionWithWallet", + "sign_raw_transaction_with_wallet", + ), + Method::new_nothing("unloadwallet", "unload_wallet"), + Method::new_modeled( + "walletcreatefundedpsbt", + "WalletCreateFundedPsbt", + "wallet_create_funded_psbt", + ), + Method::new_nothing("walletlock", "wallet_lock"), + Method::new_nothing("walletpassphrase", "wallet_passphrase"), + Method::new_nothing("walletpassphrasechange", "wallet_passphrase_change"), + Method::new_modeled("walletprocesspsbt", "WalletProcessPsbt", "wallet_process_psbt"), + Method::new_no_model("getzmqnotifications", "GetZmqNotifications", "get_zmq_notifications"), +]; diff --git a/verify/src/method/v18.rs b/verify/src/method/v18.rs new file mode 100644 index 00000000..ffde399d --- /dev/null +++ b/verify/src/method/v18.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Provides the list of v17 methods. + +use super::Method; + +pub const METHODS: &[Method] = &[ + Method::new_modeled("getbestblockhash", "GetBestBlockHash", "get_best_block_hash"), + Method::new_modeled("getblock", "GetBlock", "get_block"), + Method::new_modeled("getblockchaininfo", "GetBlockchainInfo", "get_blockchain_info"), + Method::new_modeled("getblockcount", "GetBlockCount", "get_block_count"), + Method::new_modeled("getblockhash", "GetBlockHash", "get_block_hash"), + Method::new_modeled("getblockheader", "GetBlockHeader", "get_block_header"), + Method::new_modeled("getblockstats", "GetBlockStats", "get_block_stats"), + Method::new_modeled("getchaintips", "GetChainTips", "get_chain_tips"), + Method::new_modeled("getchaintxstats", "GetChainTxStats", "get_chain_tx_stats"), + Method::new_modeled("getdifficulty", "GetDifficulty", "get_difficulty"), + Method::new_modeled("getmempoolancestors", "GetMempoolAncestors", "get_mempool_ancestors"), + Method::new_modeled( + "getmempooldescendants", + "GetMempoolDescendants", + "get_mempool_descendants", + ), + Method::new_modeled("getmempoolentry", "GetMempoolEntry", "get_mempool_entry"), + Method::new_modeled("getmempoolinfo", "GetMempoolInfo", "get_mempool_info"), + Method::new_modeled("getrawmempool", "GetRawMempool", "get_raw_mempool"), + Method::new_modeled("gettxout", "GetTxOut", "get_tx_out"), + Method::new_modeled("gettxoutproof", "GetTxOutProof", "get_tx_out_proof"), + Method::new_modeled("gettxoutsetinfo", "GetTxOutSetInfo", "get_tx_out_set_info"), + Method::new_nothing("preciousblock", "precious_block"), + Method::new_numeric("pruneblockchain", "prune_blockchain"), + Method::new_nothing("savemempool", "save_mempool"), + Method::new_modeled("scantxoutset", "ScanTxOutSet", "scan_tx_out_set"), + Method::new_bool("verifychain", "verify_chain"), + Method::new_modeled("verifytxoutproof", "VerifyTxOutProof", "verify_tx_out_proof"), + Method::new_no_model("getmemoryinfo", "GetMemoryInfo", "get_memory_info"), + Method::new_string("help", "help"), + Method::new_no_model("logging", "Logging", "logging"), + Method::new_nothing("stop", "stop"), + Method::new_numeric("uptime", "uptime"), + Method::new_modeled("generate", "Generate", "generate"), + Method::new_modeled("generatetoaddress", "GenerateToAddress", "generate_to_address"), + Method::new_none("getblocktemplate", "get_block_template"), + Method::new_none("getmininginfo", "get_mining_info"), + Method::new_none("getnetworkhashps", "get_network_hashes_per_second"), + Method::new_bool("prioritisetransaction", "prioritise_transaction"), + Method::new_none("submitblock", "submit_block"), + Method::new_nothing("addnode", "add_node"), + Method::new_nothing("clearbanned", "clear_banned"), + Method::new_nothing("disconnectnode", "disconnect_node"), + Method::new_no_model("getaddednodeinfo", "GetAddedNodeInfo", "get_added_node_info"), + Method::new_numeric("getconnectioncount", "get_connection_count"), + Method::new_no_model("getnettotals", "GetNetTotals", "get_net_totals"), + Method::new_modeled("getnetworkinfo", "GetNetworkInfo", "get_network_info"), + Method::new_no_model("getpeerinfo", "GetPeerInfo", "get_peer_info"), + Method::new_string("listbanned", "list_banned"), // v17 docs seem wrong, says no return. + Method::new_nothing("ping", "ping"), + Method::new_nothing("setban", "set_ban"), + Method::new_nothing("setnetworkactive", "set_network_active"), + Method::new_none("combinepsbt", "combine_psbt"), + Method::new_none("combinerawtransaction", "combine_raw_transaction"), + Method::new_none("converttopsbt", "convert_to_psbt"), + Method::new_none("createpsbt", "create_psbt"), + Method::new_none("createrawtransaction", "create_raw_transaction"), + Method::new_none("decodepsbt", "decode_psbt"), + Method::new_none("decoderawtransaction", "decode_raw_transaction"), + Method::new_none("decodescript", "decode_script"), + Method::new_none("finalizepsbt", "finalize_psbt"), + Method::new_none("fundrawtransaction", "fund_raw_transaciton"), + Method::new_none("getrawtransaction", "get_raw_transaction"), + Method::new_modeled("sendrawtransaction", "SendRawTransaction", "send_raw_transaction"), + Method::new_none("signrawtransactionwithkey", "sign_raw_transaction_with_key"), + Method::new_none("testmempoolaccept", "test_mempool_accept"), + Method::new_modeled("createmultisig", "CreateMultisig", "create_multisig"), + Method::new_nothing("estimatesmartfee", "estimate_smart_fee"), + Method::new_string("signmessagewithprivkey", "sign_message_with_priv_key"), + Method::new_modeled("validateaddress", "ValidateAddress", "validate_address"), + Method::new_bool("verifymessage", "verify_message"), + Method::new_nothing("abandontransaction", "abandon_transaction"), + Method::new_nothing("abortrescan", "abort_rescan"), + Method::new_modeled("addmultisigaddress", "AddMultisigAddress", "add_multisig_address"), + Method::new_nothing("backupwallet", "backup_wallet"), + Method::new_modeled("bumpfee", "BumpFee", "bump_fee"), + Method::new_modeled("createwallet", "CreateWallet", "create_wallet"), + Method::new_modeled("dumpprivkey", "DumpPrivKey", "dump_priv_key"), + Method::new_modeled("dumpwallet", "DumpWallet", "dump_wallet"), + Method::new_nothing("encryptwallet", "encrypt_wallet"), + Method::new_modeled("getaddressesbylabel", "GetAddressesByLabel", "get_addresses_by_label"), + Method::new_modeled("getaddressinfo", "GetAddressInfo", "get_address_info"), + Method::new_modeled("getbalance", "GetBalance", "get_balance"), + Method::new_modeled("getnewaddress", "GetNewAddress", "get_new_address"), + Method::new_modeled("getrawchangeaddress", "GetRawChangeAddress", "get_raw_change_address"), + Method::new_modeled("getreceivedbyaddress", "GetReceivedByAddress", "get_received_by_address"), + Method::new_modeled("gettransaction", "GetTransaction", "get_transaction"), + Method::new_modeled( + "getunconfirmedbalance", + "GetUnconfirmedBalance", + "get_unconfirmed_balance", + ), + Method::new_modeled("getwalletinfo", "GetWalletInfo", "get_wallet_info"), + Method::new_nothing("importaddress", "import_addressss"), + Method::new_nothing("importmulti", "import_multi"), + Method::new_nothing("importprivkey", "import_priv_key"), + Method::new_nothing("importprunedfunds", "import_pruned_funds"), + Method::new_nothing("importpubkey", "import_pubkey"), + Method::new_nothing("importwallet", "import_walet"), + Method::new_nothing("keypoolrefill", "keypool_refill"), + Method::new_modeled("listaddressgroupings", "ListAddressGroupings", "list_address_groupings"), + Method::new_modeled("listlabels", "ListLabels", "list_labels"), + Method::new_modeled("listlockunspent", "ListLockUnspent", "list_lock_unspent"), + Method::new_modeled( + "listreceivedbyaddress", + "ListReceivedByAddress", + "list_received_by_address", + ), + Method::new_modeled("listsinceblock", "ListSinceBlock", "list_since_block"), + Method::new_modeled("listtransactions", "ListTransactions", "list_transactions"), + Method::new_modeled("listunspent", "ListUnspent", "list_unspent"), + Method::new_modeled("listwallets", "ListWallets", "list_wallets"), + Method::new_modeled("loadwallet", "LoadWallet", "load_wallet"), + Method::new_bool("lockunspent", "lock_unspent"), + Method::new_nothing("removeprunedfunds", "remove_pruned_funds"), + Method::new_modeled("rescanblockchain", "RescanBlockchain", "rescan_blockchain"), + Method::new_modeled("sendmany", "SendMany", "send_many"), + Method::new_modeled("sendtoaddress", "SendToAddress", "send_to_address"), + Method::new_nothing("sethdseed", "set_hd_seed"), + Method::new_bool("settxfee", "set_tx_fee"), + Method::new_modeled("signmessage", "SignMessage", "sign_message"), + Method::new_modeled( + "signrawtransactionwithwallet", + "SignRawTransactionWithWallet", + "sign_raw_transaction_with_wallet", + ), + Method::new_nothing("unloadwallet", "unload_wallet"), + Method::new_modeled( + "walletcreatefundedpsbt", + "WalletCreateFundedPsbt", + "wallet_create_funded_psbt", + ), + Method::new_nothing("walletlock", "wallet_lock"), + Method::new_nothing("walletpassphrase", "wallet_passphrase"), + Method::new_nothing("walletpassphrasechange", "wallet_passphrase_change"), + Method::new_modeled("walletprocesspsbt", "WalletProcessPsbt", "wallet_process_psbt"), + Method::new_no_model("getzmqnotifications", "GetZmqNotifications", "get_zmq_notifications"), + Method::new_no_model("getrpcinfo", "GetRpcInfo", "get_rpc_info"), + Method::new_nothing("submitheader", "submit_header"), + Method::new_modeled("analyzepsbt", "AnalyzePsbt", "analyze_psbt"), + Method::new_no_model("getnodeaddresses", "GetNodeAddresses", "get_node_addresses"), + Method::new_modeled("joinpsbts", "JoinPsbts", "join_psbts"), + Method::new_modeled("utxoupdatepsbt", "UtxoUpdatePsbt", "utxo_update_psbt"), + Method::new_modeled("deriveaddresses", "DeriveAddresses", "derive_addresses"), + Method::new_no_model("getdescriptorinfo", "GetDescriptorInfo", "get_descriptor_info"), + Method::new_modeled("getreceivedbylabel", "GetReceivedByLabel", "get_received_by_label"), + Method::new_modeled("listreceivedbylabel", "ListReceivedByLabel", "list_received_by_label"), + Method::new_no_model("listwalletdir", "ListWalletDir", "list_wallet_dir"), + Method::new_nothing("setlabel", "set_label"), +]; diff --git a/verify/src/model.rs b/verify/src/model.rs new file mode 100644 index 00000000..00f35894 --- /dev/null +++ b/verify/src/model.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Things related to parsing the model files. +//! +//! The "model files" are the files in `types/src/model/`. + +use std::path::PathBuf; + +use anyhow::Result; + +use crate::method::{self, Return}; +use crate::Version; + +/// Path to the model module file. +fn path() -> PathBuf { PathBuf::from("../types/src/model/mod.rs") } + +/// Checks that a type exists in `model` module. +pub fn type_exists(version: Version, method_name: &str) -> Result { + let method = match method::Method::from_name(version, method_name) { + Some(m) => m, + None => + return Err(anyhow::Error::msg(format!( + "model type for method not found: {}", + method_name + ))), + }; + + if let Some(Return::Type(s)) = method.ret { + if method.requires_model { + return crate::grep_for_string(&path(), s); + } else { + return Ok(true); + } + } + Ok(false) +} diff --git a/verify/src/ssot.rs b/verify/src/ssot.rs new file mode 100644 index 00000000..8dc29fbc --- /dev/null +++ b/verify/src/ssot.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Things related to parsing the JSON RPC Single Source Of Truth (SSOT) file. +//! +//! The SSOT used by this tool is a file that contains the output of +//! +//! `bitcoin-cli --help` +//! +//! Run against the version of Core that we are verifying. + +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use regex::Regex; + +use crate::Version; + +/// Path to the RPC SSOT file. +pub fn path(version: Version) -> PathBuf { PathBuf::from(format!("./rpc-api-{}.txt", version)) } + +/// Parses the Bitcoin Core docs (from SSOT file) and gets all the method names. +pub fn all_methods(version: Version) -> Result> { + let path = path(version); + let file = File::open(&path) + .with_context(|| format!("Failed to grep for method names in {}", path.display()))?; + let reader = io::BufReader::new(file); + + let header_re = Regex::new(r"==").unwrap(); + let empty_re = Regex::new(r"^$").unwrap(); + + let mut methods = Vec::new(); + + for line in reader.lines() { + let line = line?; + + if header_re.is_match(&line) || empty_re.is_match(&line) { + continue; + } + + let parts: Vec<&str> = line.split_whitespace().collect(); + // We know this is not empty because of `empty_re`. + methods.push(parts[0].to_string()); + } + + Ok(methods) +} diff --git a/verify/src/versioned.rs b/verify/src/versioned.rs new file mode 100644 index 00000000..2512ec08 --- /dev/null +++ b/verify/src/versioned.rs @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Things related to parsing the version specific module file. +//! +//! The "version specific module file" is for example `types/src/v17/mod.rs`. + +use std::fmt; +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::{Context, Result}; +use regex::Regex; + +use crate::method::{self, Return}; +use crate::Version; + +/// Path to the version specific module file. +pub fn path(version: Version) -> PathBuf { + PathBuf::from(format!("../types/src/{}/mod.rs", version)) +} + +/// Parses module rustdocs and gets all the method names. +pub fn all_methods(version: Version) -> Result> { + let methods = methods_and_status(version)?; + Ok(methods.iter().map(|m| m.name.to_string()).collect()) +} + +/// Parses module rustdocs and grabs each method and its current status. +pub fn methods_and_status(version: Version) -> Result> { + let path = path(version); + let file = File::open(&path) + .with_context(|| format!("Failed to grep rustdocs in {}", path.display()))?; + let reader = io::BufReader::new(file); + + // let re = Regex::new(r"\/\/\! \| ([a-z]+) \| ([.*?]) \|").unwrap(); + let re = Regex::new(r"\/\/\! \| ([a-z]+) .* \| ([a-z ()]+?) \|").unwrap(); + + let mut methods = Vec::new(); + + for line in reader.lines() { + let line = line?; + + if let Some(caps) = re.captures(&line) { + let name = caps.get(1).unwrap().as_str(); + let status = caps.get(2).unwrap().as_str(); + let status = status.trim().parse::()?; + methods.push(Method { name: name.to_string(), status }); + } + } + Ok(methods) +} + +/// Checks that a type exists in version specific module. +pub fn return_type_exists(version: Version, method_name: &str) -> Result { + let path = path(version); + let method = match method::Method::from_name(version, method_name) { + Some(m) => m, + None => + return Err(anyhow::Error::msg(format!( + "return type for method not found: {}", + method_name + ))), + }; + if let Some(Return::Type(s)) = method.ret { + return crate::grep_for_string(&path, s); + } + Ok(false) +} + +/// A list item from rustdocs (e.g. in in `types/src/v17/mod.rs`). +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +// TODO: This name is overloaded (`method::Method`). +pub struct Method { + /// The JSON RPC method name. + pub name: String, + /// The current implementation status for this method. + pub status: Status, +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} (status: {})", self.name, self.status) + } +} + +/// Possible status for a method. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Status { + /// Done - implemented and tested. + Done, + /// Intentionally omitted. + Omitted, + /// Implemented but not yet tested. + Untested, + /// Still to do. + Todo, +} + +impl FromStr for Status { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "done" => Ok(Status::Done), + "omitted" => Ok(Status::Omitted), + "done (untested)" => Ok(Status::Untested), + "todo" => Ok(Status::Todo), + other => Err(anyhow::Error::msg(format!("unknown status: '{}'", other))), + } + } +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Status::*; + let s = match self { + Done => "done", + Omitted => "omitted", + Untested => "done (untested)", + Todo => "todo", + }; + fmt::Display::fmt(&s, f) + } +}