diff --git a/.vscode/chainhook.toml b/.vscode/chainhook.toml new file mode 100644 index 000000000..a01390bea --- /dev/null +++ b/.vscode/chainhook.toml @@ -0,0 +1,46 @@ +[storage] +working_dir = "cache" + +# The HTTP API allows you to register / deregister +# predicates dynamically. +# This is disabled by default. +# +[http_api] +http_port = 20456 +database_uri = "redis://localhost:6379/" + +[network] +mode = "testnet" +bitcoind_rpc_url = "http://localhost:18443" +bitcoind_rpc_username = "btc" +bitcoind_rpc_password = "btc" + +# Chainhook must be able to receive Bitcoin block events. +# These events can originate from either a Stacks node or a Bitcoin node's ZeroMQ interface. + +# By default, the service is set to receive Bitcoin block events from the Stacks node: +stacks_node_rpc_url = "http://localhost:20443" +stacks_events_ingestion_port = 20455 + +# However, events can also be received directly from a Bitcoin node. +# To achieve this, comment out the `stacks_node_rpc_url` line and uncomment the following line: +# bitcoind_zmq_url = "tcp://0.0.0.0:18543" + +[limits] +max_number_of_bitcoin_predicates = 100 +max_number_of_concurrent_bitcoin_scans = 100 +max_number_of_stacks_predicates = 10 +max_number_of_concurrent_stacks_scans = 10 +max_number_of_processing_threads = 16 +max_number_of_networking_threads = 16 +max_caching_memory_size_mb = 32000 + +# The TSV file is required for downloading historical data for your predicates. +# If this is not a requirement, you can comment out the `tsv_file_url` line. +# [[event_source]] +# tsv_file_url = "https://archive.hiro.so/regtest/stacks-blockchain-api/regtest-stacks-blockchain-api-latest" + +# Enables a server that provides metrics that can be scraped by Prometheus. +# This is disabled by default. +# [monitoring] +# prometheus_monitoring_port = 20457 diff --git a/.vscode/launch.json b/.vscode/launch.json index 448c74c77..9d0644e2c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,30 @@ { "version": "0.2.0", "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'chainhook'", + "cargo": { + "args": [ + "build", + "--bin=chainhook", + "--package=chainhook" + ], + "filter": { + "name": "chainhook", + "kind": "bin" + } + }, + "args": [ + "service", + "start", + "--config-path=${workspaceFolder}/.vscode/chainhook.toml", + ], + "cwd": "${workspaceFolder}", + "preLaunchTask": "redis:start", + "postDebugTask": "redis:stop" + }, { "type": "lldb", "request": "launch", diff --git a/Cargo.lock b/Cargo.lock index 23d5c23e3..989eeb269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3549,8 +3549,8 @@ dependencies = [ [[package]] name = "stacks-codec" -version = "2.9.0" -source = "git+https://github.com/hirosystems/clarinet.git?rev=b0683675115562d719ed4b5245f620e0990030a0#b0683675115562d719ed4b5245f620e0990030a0" +version = "2.10.0" +source = "git+https://github.com/hirosystems/clarinet.git?rev=fcebfb5a986ded32d5a450c34f8e5e5f2da97de4#fcebfb5a986ded32d5a450c34f8e5e5f2da97de4" dependencies = [ "clarity", "serde", diff --git a/Cargo.toml b/Cargo.toml index e7694ce18..97b5fc91f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,4 @@ default-members = ["components/chainhook-cli", "components/chainhook-sdk"] resolver = "2" [patch.crates-io] -stacks-codec = { git = "https://github.com/hirosystems/clarinet.git", rev = "b0683675115562d719ed4b5245f620e0990030a0" } +stacks-codec = { git = "https://github.com/hirosystems/clarinet.git", rev = "fcebfb5a986ded32d5a450c34f8e5e5f2da97de4" } diff --git a/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs b/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs index 883853689..f4e064e53 100644 --- a/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs +++ b/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs @@ -264,6 +264,7 @@ pub fn create_stacks_new_block( tenure_height: Some(1122), signer_bitvec: Some("000800000001ff".to_owned()), signer_signature: Some(vec!["1234".to_owned(), "2345".to_owned()]), + signer_signature_hash: None, cycle_number: Some(1), reward_set: Some(RewardSet { pox_ustx_threshold: "50000".to_owned(), diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index 1eb2c7fbf..a59344ea4 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -873,7 +873,10 @@ pub fn evaluate_stacks_predicate_on_non_consensus_events<'a>( | StacksPredicate::NftEvent(_) | StacksPredicate::StxEvent(_) | StacksPredicate::PrintEvent(_) - | StacksPredicate::Txid(_) => unreachable!(), + | StacksPredicate::Txid(_) => { + // Ignore, possibly expected behavior? + // https://github.com/hirosystems/chainhook/pull/663#discussion_r1814995429 + }, }; } (occurrences, expired_predicates) @@ -1107,7 +1110,7 @@ fn serialize_stacks_non_consensus_event( }; json!({ "payload": payload, - "received_at": event.received_at_ms, + "received_at_ms": event.received_at_ms, "received_at_block": event.received_at_block, }) } diff --git a/components/chainhook-sdk/src/chainhooks/tests/fixtures/stacks/testnet/occurrence.json b/components/chainhook-sdk/src/chainhooks/tests/fixtures/stacks/testnet/occurrence.json index d1f9474b3..51ce27c9e 100644 --- a/components/chainhook-sdk/src/chainhooks/tests/fixtures/stacks/testnet/occurrence.json +++ b/components/chainhook-sdk/src/chainhooks/tests/fixtures/stacks/testnet/occurrence.json @@ -20,7 +20,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -108,7 +109,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -195,7 +197,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -283,7 +286,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -370,7 +374,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -459,7 +464,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -547,7 +553,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -635,7 +642,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -724,7 +732,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -812,7 +821,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -900,7 +910,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -988,7 +999,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -1076,7 +1088,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", @@ -1174,7 +1187,8 @@ "cycle_number": null, "reward_set": null, "signer_bitvec": null, - "signer_signature": null + "signer_signature": null, + "signer_public_keys": null }, "parent_block_identifier": { "hash": "0x", diff --git a/components/chainhook-sdk/src/indexer/stacks/mod.rs b/components/chainhook-sdk/src/indexer/stacks/mod.rs index 1d6a4f844..7ebf90c16 100644 --- a/components/chainhook-sdk/src/indexer/stacks/mod.rs +++ b/components/chainhook-sdk/src/indexer/stacks/mod.rs @@ -45,6 +45,9 @@ pub struct NewBlock { #[serde(skip_serializing_if = "Option::is_none")] pub signer_bitvec: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub signer_signature_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub signer_signature: Option>, @@ -472,6 +475,13 @@ pub fn standardize_stacks_block( }) }; + let signer_sig_hash = block + .signer_signature_hash + .as_ref() + .map(|hash| { + hex::decode(&hash[2..]).expect("unable to decode signer_signature hex") + }); + let block = StacksBlockData { block_identifier: BlockIdentifier { hash: block.index_block_hash.clone(), @@ -502,6 +512,20 @@ pub fn standardize_stacks_block( signer_bitvec: block.signer_bitvec.clone(), signer_signature: block.signer_signature.clone(), + signer_public_keys: match (signer_sig_hash, &block.signer_signature) { + (Some(signer_sig_hash), Some(signatures)) => { + Some(signatures.iter().map(|sig_hex| { + let sig_msg = clarity::util::secp256k1::MessageSignature::from_hex(sig_hex) + .map_err(|e| format!("unable to parse signer signature message: {}", e))?; + let pubkey = get_signer_pubkey_from_message_hash(&signer_sig_hash, &sig_msg) + .map_err(|e| format!("unable to recover signer sig pubkey: {}", e))?; + Ok(format!("0x{}", hex::encode(pubkey))) + }) + .collect::, String>>()?) + } + _ => None, + }, + cycle_number: block.cycle_number, reward_set: block.reward_set.as_ref().and_then(|r| { Some(StacksBlockMetadataRewardSet { @@ -690,10 +714,13 @@ pub fn standardize_stacks_stackerdb_chunks( }) } SignerMessage::BlockResponse(block_response) => match block_response { - BlockResponse::Accepted((block_hash, sig)) => StacksSignerMessage::BlockResponse( + BlockResponse::Accepted(block_accepted) => StacksSignerMessage::BlockResponse( BlockResponseData::Accepted(BlockAcceptedResponse { - signer_signature_hash: format!("0x{}", block_hash.to_hex()), - sig: format!("0x{}", sig.to_hex()), + signer_signature_hash: format!("0x{}", block_accepted.signer_signature_hash.to_hex()), + signature: format!("0x{}", block_accepted.signature.to_hex()), + metadata: SignerMessageMetadata { + server_version: block_accepted.metadata.server_version, + } }), ), BlockResponse::Rejected(block_rejection) => StacksSignerMessage::BlockResponse( @@ -701,8 +728,8 @@ pub fn standardize_stacks_stackerdb_chunks( reason: block_rejection.reason, reason_code: match block_rejection.reason_code { RejectCode::ValidationFailed(validate_reject_code) => { - BlockRejectReasonCode::ValidationFailed( - match validate_reject_code { + BlockRejectReasonCode::ValidationFailed { + validation_failed: match validate_reject_code { ValidateRejectCode::BadBlockHash => { BlockValidationFailedCode::BadBlockHash } @@ -725,7 +752,7 @@ pub fn standardize_stacks_stackerdb_chunks( BlockValidationFailedCode::NoSuchTenure } }, - ) + } } RejectCode::NoSortitionView => BlockRejectReasonCode::NoSortitionView, RejectCode::ConnectivityIssues => { @@ -745,6 +772,9 @@ pub fn standardize_stacks_stackerdb_chunks( ), chain_id: block_rejection.chain_id, signature: format!("0x{}", block_rejection.signature.to_hex()), + metadata: SignerMessageMetadata { + server_version: block_rejection.metadata.server_version, + }, }), ), }, @@ -848,6 +878,36 @@ fn get_nakamoto_index_block_hash( Ok(format!("0x{}", hex::encode(hash))) } +pub fn get_signer_pubkey_from_message_hash( + message_hash: &Vec, + signature: &clarity::util::secp256k1::MessageSignature, +) -> Result<[u8; 33], String> { + use miniscript::bitcoin::{ + key::Secp256k1, + secp256k1::{ + ecdsa::{RecoverableSignature, RecoveryId}, + Message, + }, + }; + + let (first, sig) = signature.0.split_at(1); + let rec_id = first[0]; + + let secp = Secp256k1::new(); + let recovery_id = + RecoveryId::from_i32(rec_id as i32).map_err(|e| format!("invalid recovery id: {e}"))?; + let signature = RecoverableSignature::from_compact(&sig, recovery_id) + .map_err(|e| format!("invalid signature: {e}"))?; + let message = + Message::from_digest_slice(&message_hash).map_err(|e| format!("invalid digest message: {e}"))?; + + let pubkey = secp + .recover_ecdsa(&message, &signature) + .map_err(|e| format!("unable to recover pubkey: {e}"))?; + + Ok(pubkey.serialize()) +} + #[cfg(feature = "stacks-signers")] pub fn get_signer_pubkey_from_stackerdb_chunk_slot( slot: &NewSignerModifiedSlot, diff --git a/components/chainhook-sdk/src/indexer/stacks/tests.rs b/components/chainhook-sdk/src/indexer/stacks/tests.rs index 493f6fd5e..f4a005c97 100644 --- a/components/chainhook-sdk/src/indexer/stacks/tests.rs +++ b/components/chainhook-sdk/src/indexer/stacks/tests.rs @@ -440,7 +440,7 @@ fn parses_block_response_signer_message() { match &message.message { StacksSignerMessage::BlockResponse(response) => match response { BlockResponseData::Accepted(accepted) => { - assert_eq!(accepted.sig, "0x00a1c66742e665e981d10f7a70a5df312c9cba729331129ff1b510e71133d79c0122b25266bf47e8c1c923b4fde0464756ced884030e9983f797c902961fc9b0b1"); + assert_eq!(accepted.signature, "0x00a1c66742e665e981d10f7a70a5df312c9cba729331129ff1b510e71133d79c0122b25266bf47e8c1c923b4fde0464756ced884030e9983f797c902961fc9b0b1"); assert_eq!( accepted.signer_signature_hash, "0x8f913dd2bcc2cfbd1c82166e0ad99230f76de098a5ba6ee1b15b042c8f67c6f0" diff --git a/components/chainhook-sdk/src/indexer/tests/helpers/stacks_blocks.rs b/components/chainhook-sdk/src/indexer/tests/helpers/stacks_blocks.rs index 71123b9fe..fcbd1e795 100644 --- a/components/chainhook-sdk/src/indexer/tests/helpers/stacks_blocks.rs +++ b/components/chainhook-sdk/src/indexer/tests/helpers/stacks_blocks.rs @@ -76,6 +76,7 @@ pub fn generate_test_stacks_block( tenure_height: Some(1122), signer_bitvec: Some("1010101010101".to_owned()), signer_signature: Some(vec!["1234".to_owned(), "2345".to_owned()]), + signer_public_keys: Some(vec!["12".to_owned(), "23".to_owned()]), cycle_number: Some(1), reward_set: Some(StacksBlockMetadataRewardSet { pox_ustx_threshold: "50000".to_owned(), diff --git a/components/chainhook-types-js/src/index.ts b/components/chainhook-types-js/src/index.ts index 8f6251d32..5c51c5308 100644 --- a/components/chainhook-types-js/src/index.ts +++ b/components/chainhook-types-js/src/index.ts @@ -699,6 +699,7 @@ export interface StacksBlockMetadata { tenure_height?: number | null; signer_bitvec?: string | null; signer_signature?: string[] | null; + signer_public_keys?: string[] | null; cycle_number?: number | null; reward_set?: { pox_ustx_threshold: string; diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index ee9066b9f..6af1972b4 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -119,6 +119,7 @@ pub struct StacksBlockMetadata { pub block_time: Option, pub signer_bitvec: Option, pub signer_signature: Option>, + pub signer_public_keys: Option>, // Available starting in epoch3, only included in blocks where the pox cycle rewards are first calculated pub cycle_number: Option, diff --git a/components/chainhook-types-rs/src/signers.rs b/components/chainhook-types-rs/src/signers.rs index 131a32b67..c0754dd28 100644 --- a/components/chainhook-types-rs/src/signers.rs +++ b/components/chainhook-types-rs/src/signers.rs @@ -33,10 +33,17 @@ pub struct BlockProposalData { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct BlockAcceptedResponse { pub signer_signature_hash: String, - pub sig: String, + pub signature: String, + pub metadata: SignerMessageMetadata, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct SignerMessageMetadata { + pub server_version: String, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BlockValidationFailedCode { BadBlockHash, BadTransaction, @@ -50,7 +57,11 @@ pub enum BlockValidationFailedCode { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BlockRejectReasonCode { - ValidationFailed(BlockValidationFailedCode), + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + ValidationFailed { + #[serde(rename = "VALIDATION_FAILED")] + validation_failed: BlockValidationFailedCode, + }, ConnectivityIssues, RejectedInPriorRound, NoSortitionView, @@ -65,6 +76,7 @@ pub struct BlockRejectedResponse { pub signer_signature_hash: String, pub chain_id: u32, pub signature: String, + pub metadata: SignerMessageMetadata, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] diff --git a/components/client/typescript/src/schemas/stacks/payload.ts b/components/client/typescript/src/schemas/stacks/payload.ts index e25974bf8..b87683167 100644 --- a/components/client/typescript/src/schemas/stacks/payload.ts +++ b/components/client/typescript/src/schemas/stacks/payload.ts @@ -74,6 +74,7 @@ export const StacksEventMetadataSchema = Type.Object({ block_time: Nullable(Type.Integer()), signer_bitvec: Nullable(Type.String()), signer_signature: Nullable(Type.Array(Type.String())), + signer_public_keys: Nullable(Type.Array(Type.String())), // Available starting in epoch3, only included in blocks where the pox cycle rewards are first calculated cycle_number: Nullable(Type.Integer()), diff --git a/components/client/typescript/src/schemas/stacks/signers.ts b/components/client/typescript/src/schemas/stacks/signers.ts index caa0e2e3a..7b9ceeb8f 100644 --- a/components/client/typescript/src/schemas/stacks/signers.ts +++ b/components/client/typescript/src/schemas/stacks/signers.ts @@ -40,7 +40,10 @@ export const StacksSignerMessageBlockResponseAcceptedSchema = Type.Object({ type: Type.Literal('Accepted'), data: Type.Object({ signer_signature_hash: Type.String(), - sig: Type.String(), + signature: Type.String(), + metadata: Type.Object({ + server_version: Type.String(), + }), }), }); export type StacksSignerMessageBlockResponseAccepted = Static< @@ -52,7 +55,17 @@ export const StacksSignerMessageBlockResponseRejectedSchema = Type.Object({ data: Type.Object({ reason: Type.String(), reason_code: Type.Union([ - Type.Literal('VALIDATION_FAILED'), + Type.Object({ + VALIDATION_FAILED: Type.Union([ + Type.Literal('BAD_BLOCK_HASH'), + Type.Literal('BAD_TRANSACTION'), + Type.Literal('INVALID_BLOCK'), + Type.Literal('CHAINSTATE_ERROR'), + Type.Literal('UNKNOWN_PARENT'), + Type.Literal('NON_CANONICAL_TENURE'), + Type.Literal('NO_SUCH_TENURE'), + ]), + }), Type.Literal('CONNECTIVITY_ISSUES'), Type.Literal('REJECTED_IN_PRIOR_ROUND'), Type.Literal('NO_SORTITION_VIEW'), @@ -62,6 +75,9 @@ export const StacksSignerMessageBlockResponseRejectedSchema = Type.Object({ signer_signature_hash: Type.String(), chain_id: Type.Integer(), signature: Type.String(), + metadata: Type.Object({ + server_version: Type.String(), + }), }), }); export type StacksSignerMessageBlockResponseRejected = Static<