diff --git a/Cargo.lock b/Cargo.lock index 3e5a2a023..23d5c23e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3549,12 +3549,11 @@ dependencies = [ [[package]] name = "stacks-codec" -version = "2.7.0" -source = "git+https://github.com/hirosystems/clarinet.git?rev=3a2f9136abd85b265e538fbe51c808e9c09a06cb#3a2f9136abd85b265e538fbe51c808e9c09a06cb" +version = "2.9.0" +source = "git+https://github.com/hirosystems/clarinet.git?rev=b0683675115562d719ed4b5245f620e0990030a0#b0683675115562d719ed4b5245f620e0990030a0" dependencies = [ "clarity", "serde", - "wsts", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2111714ed..e7694ce18 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 = "3a2f9136abd85b265e538fbe51c808e9c09a06cb" } +stacks-codec = { git = "https://github.com/hirosystems/clarinet.git", rev = "b0683675115562d719ed4b5245f620e0990030a0" } diff --git a/components/chainhook-cli/src/scan/stacks.rs b/components/chainhook-cli/src/scan/stacks.rs index 1e3b100e5..9e021513e 100644 --- a/components/chainhook-cli/src/scan/stacks.rs +++ b/components/chainhook-cli/src/scan/stacks.rs @@ -339,6 +339,8 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( chainhook: predicate_spec, apply: hits_per_blocks, rollback: vec![], + // TODO(rafaelcr): Query for non consensus events which fall between block timestamps to fill in here + events: vec![] }; let res = match handle_stacks_hook_action( trigger, @@ -533,6 +535,8 @@ pub async fn scan_stacks_chainstate_via_csv_using_predicate( chainhook: predicate_spec, apply: hits_per_blocks, rollback: vec![], + // TODO(rafaelcr): Consider StackerDB chunks that come from TSVs. + events: vec![] }; match handle_stacks_hook_action(trigger, &proofs, &config.get_event_observer_config(), ctx) { diff --git a/components/chainhook-cli/src/service/mod.rs b/components/chainhook-cli/src/service/mod.rs index a6249a4c5..7e50be6d3 100644 --- a/components/chainhook-cli/src/service/mod.rs +++ b/components/chainhook-cli/src/service/mod.rs @@ -548,8 +548,9 @@ impl Service { } StacksChainEvent::ChainUpdatedWithMicroblocks(_) | StacksChainEvent::ChainUpdatedWithMicroblocksReorg(_) => {}, - StacksChainEvent::ChainUpdatedWithStackerDbChunks(data) => { + StacksChainEvent::ChainUpdatedWithNonConsensusEvents(data) => { // TODO(rafaelcr): Store signer data. + println!("signer message: {:?}", data); } }, Err(e) => { @@ -619,8 +620,8 @@ impl Service { } StacksChainEvent::ChainUpdatedWithMicroblocks(_) | StacksChainEvent::ChainUpdatedWithMicroblocksReorg(_) => {}, - StacksChainEvent::ChainUpdatedWithStackerDbChunks(data) => { - // TODO(rafaelcr): Send via HTTP payload. + StacksChainEvent::ChainUpdatedWithNonConsensusEvents(_) => { + // TODO(rafaelcr): Expire signer message predicates when appropriate }, }; update_status_from_report( diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index 66f2205c4..1eb2c7fbf 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -1,15 +1,15 @@ use crate::observer::EventObserverConfig; use crate::utils::{AbstractStacksBlock, Context, MAX_BLOCK_HEIGHTS_ENTRIES}; +use super::types::validate_txid; use super::types::{ append_error_context, BlockIdentifierIndexRule, ChainhookInstance, ExactMatchingRule, HookAction, }; -use super::types::validate_txid; use chainhook_types::{ - BlockIdentifier, StacksChainEvent, StacksNetwork, StacksTransactionData, - StacksTransactionEvent, StacksTransactionEventPayload, StacksTransactionKind, - TransactionIdentifier, + BlockIdentifier, StacksChainEvent, StacksNetwork, StacksNonConsensusEventData, + StacksTransactionData, StacksTransactionEvent, StacksTransactionEventPayload, + StacksTransactionKind, TransactionIdentifier, }; use clarity::codec::StacksMessageCodec; use clarity::vm::types::{ @@ -259,6 +259,8 @@ pub enum StacksPredicate { NftEvent(StacksNftEventBasedPredicate), StxEvent(StacksStxEventBasedPredicate), Txid(ExactMatchingRule), + #[cfg(feature = "stacks-signers")] + SignerMessage(StacksSignerMessagePredicate), } impl StacksPredicate { @@ -307,11 +309,28 @@ impl StacksPredicate { )); } } + #[cfg(feature = "stacks-signers")] + StacksPredicate::SignerMessage(StacksSignerMessagePredicate::FromSignerPubKey(_)) => { + // TODO(rafaelcr): Validate pubkey format + } + #[cfg(feature = "stacks-signers")] + StacksPredicate::SignerMessage(StacksSignerMessagePredicate::AfterTimestamp(_)) => {} } Ok(()) } } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StacksSignerMessagePredicate { + AfterTimestamp(u64), + FromSignerPubKey(String), +} + +impl StacksSignerMessagePredicate { + // TODO(rafaelcr): Write validators +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct StacksContractCallBasedPredicate { @@ -457,6 +476,7 @@ pub struct StacksTriggerChainhook<'a> { pub chainhook: &'a StacksChainhookInstance, pub apply: Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, pub rollback: Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, + pub events: Vec<&'a StacksNonConsensusEventData>, } #[derive(Clone, Debug)] @@ -480,21 +500,18 @@ pub struct StacksChainhookPayload { pub struct StacksChainhookOccurrencePayload { pub apply: Vec, pub rollback: Vec, + pub events: Vec, pub chainhook: StacksChainhookPayload, } impl StacksChainhookOccurrencePayload { - pub fn from_trigger( - trigger: StacksTriggerChainhook<'_>, - ) -> StacksChainhookOccurrencePayload { + pub fn from_trigger(trigger: StacksTriggerChainhook<'_>) -> StacksChainhookOccurrencePayload { StacksChainhookOccurrencePayload { apply: trigger .apply .into_iter() .map(|(transactions, block)| { - let transactions = transactions - .into_iter().cloned() - .collect::>(); + let transactions = transactions.into_iter().cloned().collect::>(); StacksApplyTransactionPayload { block_identifier: block.get_identifier().clone(), transactions, @@ -505,9 +522,7 @@ impl StacksChainhookOccurrencePayload { .rollback .into_iter() .map(|(transactions, block)| { - let transactions = transactions - .into_iter().cloned() - .collect::>(); + let transactions = transactions.into_iter().cloned().collect::>(); StacksRollbackTransactionPayload { block_identifier: block.get_identifier().clone(), transactions, @@ -517,6 +532,7 @@ impl StacksChainhookOccurrencePayload { chainhook: StacksChainhookPayload { uuid: trigger.chainhook.uuid.clone(), }, + events: trigger.events.into_iter().cloned().collect::>(), } } } @@ -593,6 +609,7 @@ pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( chainhook, apply, rollback, + events: vec![], }) } } @@ -621,6 +638,7 @@ pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( chainhook, apply, rollback, + events: vec![], }) } } @@ -657,6 +675,7 @@ pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( chainhook, apply, rollback, + events: vec![], }) } } @@ -718,13 +737,37 @@ pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( chainhook, apply, rollback, + events: vec![], }) } } - }, - StacksChainEvent::ChainUpdatedWithStackerDbChunks(data) => { - // TODO: Support predicates to send this data - }, + } + #[cfg(feature = "stacks-signers")] + StacksChainEvent::ChainUpdatedWithNonConsensusEvents(data) => { + if let Some(first_event) = data.events.first() { + for chainhook in active_chainhooks.iter() { + evaluated_predicates + .insert(chainhook.uuid.as_str(), &first_event.received_at_block); + let (occurrences, mut expirations) = + evaluate_stacks_predicate_on_non_consensus_events( + &data.events, + chainhook, + ctx, + ); + expired_predicates.append(&mut expirations); + if occurrences.len() > 0 { + triggered_predicates.push(StacksTriggerChainhook { + chainhook, + apply: vec![], + rollback: vec![], + events: occurrences, + }); + } + } + } + } + #[cfg(not(feature = "stacks-signers"))] + StacksChainEvent::ChainUpdatedWithNonConsensusEvents(_) => {} } ( triggered_predicates, @@ -795,7 +838,45 @@ pub fn evaluate_stacks_predicate_on_block<'a>( | StacksPredicate::StxEvent(_) | StacksPredicate::PrintEvent(_) | StacksPredicate::Txid(_) => unreachable!(), + #[cfg(feature = "stacks-signers")] + StacksPredicate::SignerMessage(_) => false, + } +} + +#[cfg(feature = "stacks-signers")] +pub fn evaluate_stacks_predicate_on_non_consensus_events<'a>( + events: &'a Vec, + chainhook: &'a StacksChainhookInstance, + _ctx: &Context, +) -> ( + Vec<&'a StacksNonConsensusEventData>, + BTreeMap<&'a str, &'a BlockIdentifier>, +) { + let mut occurrences = vec![]; + let expired_predicates = BTreeMap::new(); + for event in events { + match &chainhook.predicate { + StacksPredicate::SignerMessage(StacksSignerMessagePredicate::AfterTimestamp( + timestamp, + )) => { + if event.received_at_ms >= *timestamp { + occurrences.push(event); + } + } + StacksPredicate::SignerMessage(StacksSignerMessagePredicate::FromSignerPubKey(_)) => { + // TODO(rafaelcr): Evaluate on pubkey + } + StacksPredicate::BlockHeight(_) + | StacksPredicate::ContractDeployment(_) + | StacksPredicate::ContractCall(_) + | StacksPredicate::FtEvent(_) + | StacksPredicate::NftEvent(_) + | StacksPredicate::StxEvent(_) + | StacksPredicate::PrintEvent(_) + | StacksPredicate::Txid(_) => unreachable!(), + }; } + (occurrences, expired_predicates) } pub fn evaluate_stacks_predicate_on_transaction<'a>( @@ -819,7 +900,7 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>( _ => false, }, StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::ImplementTrait( - stacks_trait, + _stacks_trait, )) => match &transaction.metadata.kind { StacksTransactionKind::ContractDeployment(_actual_deployment) => { ctx.try_log(|logger| { @@ -952,7 +1033,9 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>( } StacksPredicate::PrintEvent(expected_event) => { for event in transaction.metadata.receipt.events.iter() { - if let StacksTransactionEventPayload::SmartContractEvent(actual) = &event.event_payload { + if let StacksTransactionEventPayload::SmartContractEvent(actual) = + &event.event_payload + { if actual.topic == "print" { match expected_event { StacksPrintEventBasedPredicate::Contains { @@ -1006,9 +1089,29 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>( txid.eq(&transaction.transaction_identifier.hash) } StacksPredicate::BlockHeight(_) => unreachable!(), + #[cfg(feature = "stacks-signers")] + StacksPredicate::SignerMessage(_) => false, } } +fn serialize_stacks_non_consensus_event( + event: &StacksNonConsensusEventData, + _ctx: &Context, +) -> serde_json::Value { + use chainhook_types::StacksNonConsensusEventPayloadData; + + let payload = match &event.payload { + StacksNonConsensusEventPayloadData::SignerMessage(chunk) => { + json!({"type": "SignerMessage", "data": chunk}) + } + }; + json!({ + "payload": payload, + "received_at": event.received_at_ms, + "received_at_block": event.received_at_block, + }) +} + fn serialize_stacks_block( block: &dyn AbstractStacksBlock, transactions: Vec<&StacksTransactionData>, @@ -1238,7 +1341,7 @@ pub fn serialized_decoded_clarity_value(hex_value: &str, ctx: &Context) -> serde Ok(bytes) => bytes, _ => return json!(hex_value.to_string()), }; - + match ClarityValue::consensus_deserialize(&mut Cursor::new(&value_bytes)) { Ok(value) => serialize_to_json(&value), Err(e) => { @@ -1319,6 +1422,7 @@ pub fn serialize_stacks_payload_to_json<'a>( "rollback": trigger.rollback.into_iter().map(|(transactions, block)| { serialize_stacks_block(block, transactions, decode_clarity_values, include_contract_abi, ctx) }).collect::>(), + "events": trigger.events.into_iter().map(|event| serialize_stacks_non_consensus_event(event, ctx)).collect::>(), "chainhook": { "uuid": trigger.chainhook.uuid, "predicate": trigger.chainhook.predicate, 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 9fb468e6a..55ae6171b 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 @@ -1211,5 +1211,6 @@ } ] } - ] + ], + "events": [] } \ No newline at end of file diff --git a/components/chainhook-sdk/src/chainhooks/tests/mod.rs b/components/chainhook-sdk/src/chainhooks/tests/mod.rs index 14b0774b4..7f8b7a7fa 100644 --- a/components/chainhook-sdk/src/chainhooks/tests/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/tests/mod.rs @@ -733,6 +733,7 @@ fn test_stacks_hook_action_noop() { chainhook: &chainhook, apply: vec![(apply_transactions, apply_blocks)], rollback: vec![(rollback_transactions, rollback_blocks)], + events: vec![] }; let proofs = HashMap::new(); @@ -811,6 +812,7 @@ fn test_stacks_hook_action_file_append() { chainhook: &chainhook, apply, rollback: vec![(rollback_transactions, rollback_block)], + events: vec![] }; let proofs = HashMap::new(); diff --git a/components/chainhook-sdk/src/indexer/mod.rs b/components/chainhook-sdk/src/indexer/mod.rs index af484d466..cc257dca5 100644 --- a/components/chainhook-sdk/src/indexer/mod.rs +++ b/components/chainhook-sdk/src/indexer/mod.rs @@ -171,21 +171,32 @@ impl Indexer { pub fn handle_stacks_marshalled_stackerdb_chunk( &mut self, marshalled_stackerdb_chunks: JsonValue, - receipt_time: u64, + receipt_time_ms: u128, ctx: &Context, ) -> Result, String> { - use chainhook_types::StacksChainUpdatedWithStackerDbChunksData; - + use chainhook_types::{ + StacksChainUpdatedWithNonConsensusEventsData, StacksNonConsensusEventData, + StacksNonConsensusEventPayloadData, + }; + let Some(chain_tip) = self.stacks_blocks_pool.get_canonical_fork_chain_tip() else { + return Err("StackerDB chunk received with no canonical chain tip".to_string()); + }; let chunks = stacks::standardize_stacks_marshalled_stackerdb_chunks( - &self.config, marshalled_stackerdb_chunks, - receipt_time, - &mut self.stacks_context, ctx, )?; if chunks.len() > 0 { - Ok(Some(StacksChainEvent::ChainUpdatedWithStackerDbChunks( - StacksChainUpdatedWithStackerDbChunksData { chunks }, + Ok(Some(StacksChainEvent::ChainUpdatedWithNonConsensusEvents( + StacksChainUpdatedWithNonConsensusEventsData { + events: chunks + .into_iter() + .map(|chunk| StacksNonConsensusEventData { + payload: StacksNonConsensusEventPayloadData::SignerMessage(chunk), + received_at_ms: receipt_time_ms as u64, + received_at_block: chain_tip.clone(), + }) + .collect(), + }, ))) } else { Ok(None) diff --git a/components/chainhook-sdk/src/indexer/stacks/blocks_pool.rs b/components/chainhook-sdk/src/indexer/stacks/blocks_pool.rs index 5897144c2..80cf44b94 100644 --- a/components/chainhook-sdk/src/indexer/stacks/blocks_pool.rs +++ b/components/chainhook-sdk/src/indexer/stacks/blocks_pool.rs @@ -49,6 +49,15 @@ impl StacksBlockPool { } } + pub fn get_canonical_fork_chain_tip(&self) -> Option<&BlockIdentifier> { + match self.forks.get(&self.canonical_fork_id) { + Some(fork) => { + Some(fork.get_tip()) + }, + None => None, + } + } + pub fn seed_block_pool(&mut self, blocks: Vec, ctx: &Context) { ctx.try_log(|logger| { slog::info!(logger, "Seeding block pool with {} blocks", blocks.len()) diff --git a/components/chainhook-sdk/src/indexer/stacks/mod.rs b/components/chainhook-sdk/src/indexer/stacks/mod.rs index c935adbe4..281aa9f12 100644 --- a/components/chainhook-sdk/src/indexer/stacks/mod.rs +++ b/components/chainhook-sdk/src/indexer/stacks/mod.rs @@ -12,7 +12,7 @@ use clarity::vm::types::{SequenceData, Value as ClarityValue}; use hiro_system_kit::slog; use rocket::serde::json::Value as JsonValue; use rocket::serde::Deserialize; -use stacks_codec::codec::{NakamotoBlock, StacksTransaction, TransactionAuth, TransactionPayload}; +use stacks_codec::codec::{StacksTransaction, TransactionAuth, TransactionPayload}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::convert::TryInto; use std::io::Cursor; @@ -38,10 +38,10 @@ pub struct NewBlock { #[serde(skip_serializing_if = "Option::is_none")] pub block_time: Option, - + #[serde(skip_serializing_if = "Option::is_none")] pub signer_bitvec: Option, - + #[serde(skip_serializing_if = "Option::is_none")] pub signer_signature: Option>, @@ -305,16 +305,17 @@ pub struct ContractReadonlyCall { #[cfg(feature = "stacks-signers")] #[derive(Deserialize, Debug)] -pub struct NewStackerDbChunkIssuer { - pub issuer_id: u32, - pub slots: Vec, -} +pub struct NewStackerDbChunkIssuerId(pub u32); + +#[cfg(feature = "stacks-signers")] +#[derive(Deserialize, Debug)] +pub struct NewStackerDbChunkIssuerSlots(pub Vec); #[cfg(feature = "stacks-signers")] #[derive(Deserialize, Debug)] pub struct NewStackerDbChunksContractId { pub name: String, - pub issuer: Vec, + pub issuer: (NewStackerDbChunkIssuerId, NewStackerDbChunkIssuerSlots), } #[cfg(feature = "stacks-signers")] @@ -323,7 +324,7 @@ pub struct NewSignerModifiedSlot { pub sig: String, pub data: String, pub slot_id: u64, - pub version: u64, + pub slot_version: u64, } #[cfg(feature = "stacks-signers")] @@ -646,22 +647,18 @@ pub fn standardize_stacks_microblock_trail( #[cfg(feature = "stacks-signers")] pub fn standardize_stacks_marshalled_stackerdb_chunks( - _indexer_config: &IndexerConfig, marshalled_stackerdb_chunks: JsonValue, - receipt_time: u64, - _chain_ctx: &mut StacksChainContext, _ctx: &Context, ) -> Result, String> { let mut stackerdb_chunks: NewStackerDbChunks = serde_json::from_value(marshalled_stackerdb_chunks) .map_err(|e| format!("unable to parse stackerdb chunks {e}"))?; - standardize_stacks_stackerdb_chunks(&mut stackerdb_chunks, receipt_time) + standardize_stacks_stackerdb_chunks(&mut stackerdb_chunks) } #[cfg(feature = "stacks-signers")] pub fn standardize_stacks_stackerdb_chunks( stackerdb_chunks: &NewStackerDbChunks, - receipt_time: u64, ) -> Result, String> { use stacks_codec::codec::BlockResponse; use stacks_codec::codec::RejectCode; @@ -683,7 +680,7 @@ pub fn standardize_stacks_stackerdb_chunks( let message = match signer_message { SignerMessage::BlockProposal(block_proposal) => { StacksSignerMessage::BlockProposal(BlockProposalData { - block: standardize_stacks_nakamoto_block(&block_proposal.block), + block: standardize_stacks_nakamoto_block(&block_proposal.block)?, burn_height: block_proposal.burn_height, reward_cycle: block_proposal.reward_cycle, }) @@ -691,8 +688,8 @@ pub fn standardize_stacks_stackerdb_chunks( SignerMessage::BlockResponse(block_response) => match block_response { BlockResponse::Accepted((block_hash, sig)) => StacksSignerMessage::BlockResponse( BlockResponseData::Accepted(BlockAcceptedResponse { - signer_signature_hash: block_hash.to_hex(), - sig: sig.to_hex(), + signer_signature_hash: format!("0x{}", block_hash.to_hex()), + sig: format!("0x{}", sig.to_hex()), }), ), BlockResponse::Rejected(block_rejection) => StacksSignerMessage::BlockResponse( @@ -738,15 +735,18 @@ pub fn standardize_stacks_stackerdb_chunks( } RejectCode::TestingDirective => BlockRejectReasonCode::TestingDirective, }, - signer_signature_hash: block_rejection.signer_signature_hash.to_hex(), + signer_signature_hash: format!( + "0x{}", + block_rejection.signer_signature_hash.to_hex() + ), chain_id: block_rejection.chain_id, - signature: block_rejection.signature.to_hex(), + signature: format!("0x{}", block_rejection.signature.to_hex()), }), ), }, SignerMessage::BlockPushed(nakamoto_block) => { StacksSignerMessage::BlockPushed(BlockPushedData { - block: standardize_stacks_nakamoto_block(&nakamoto_block), + block: standardize_stacks_nakamoto_block(&nakamoto_block)?, }) } SignerMessage::MockSignature(_) @@ -757,10 +757,12 @@ pub fn standardize_stacks_stackerdb_chunks( }; parsed_chunks.push(StacksStackerDbChunk { contract: contract_id.clone(), - sig: slot.sig.clone(), - pubkey: get_signer_pubkey_from_stackerdb_chunk_slot(slot, &data_bytes)?, + sig: format!("0x{}", slot.sig), + pubkey: format!( + "0x{}", + get_signer_pubkey_from_stackerdb_chunk_slot(slot, &data_bytes)? + ), message, - receipt_time, }); } @@ -768,36 +770,78 @@ pub fn standardize_stacks_stackerdb_chunks( } #[cfg(feature = "stacks-signers")] -pub fn standardize_stacks_nakamoto_block(block: &NakamotoBlock) -> NakamotoBlockData { +pub fn standardize_stacks_nakamoto_block( + block: &stacks_codec::codec::NakamotoBlock, +) -> Result { use miniscript::bitcoin::hex::Case; use miniscript::bitcoin::hex::DisplayHex; - NakamotoBlockData { + let block_hash = get_nakamoto_block_hash(block)?; + Ok(NakamotoBlockData { header: NakamotoBlockHeaderData { version: block.header.version, chain_length: block.header.chain_length, burn_spent: block.header.burn_spent, - consensus_hash: block.header.consensus_hash.to_hex(), - parent_block_id: block.header.parent_block_id.to_hex(), - tx_merkle_root: block.header.tx_merkle_root.to_hex(), - state_index_root: block.header.state_index_root.to_hex(), + consensus_hash: format!("0x{}", block.header.consensus_hash.to_hex()), + parent_block_id: format!("0x{}", block.header.parent_block_id.to_hex()), + tx_merkle_root: format!("0x{}", block.header.tx_merkle_root.to_hex()), + state_index_root: format!("0x{}", block.header.state_index_root.to_hex()), timestamp: block.header.timestamp, - miner_signature: block.header.miner_signature.to_hex(), + miner_signature: format!("0x{}", block.header.miner_signature.to_hex()), signer_signature: block .header .signer_signature .iter() - .map(|s| s.to_hex()) + .map(|s| format!("0x{}", s.to_hex())) .collect(), - pox_treatment: block - .header - .pox_treatment - .serialize_to_vec() - .to_hex_string(Case::Lower), + pox_treatment: format!( + "0x{}", + block + .header + .pox_treatment + .serialize_to_vec() + .to_hex_string(Case::Lower) + ), }, + block_hash: block_hash.clone(), + index_block_hash: get_nakamoto_index_block_hash(&block_hash, &block.header.consensus_hash)?, // TODO(rafaelcr): Parse and return transactions. transactions: vec![], - } + }) +} + +#[cfg(feature = "stacks-signers")] +fn get_nakamoto_block_hash(block: &stacks_codec::codec::NakamotoBlock) -> Result { + use clarity::util::hash::Sha512Trunc256Sum; + + let mut block_header_bytes = vec![block.header.version]; + block_header_bytes.extend(block.header.chain_length.to_be_bytes()); + block_header_bytes.extend(block.header.burn_spent.to_be_bytes()); + block_header_bytes.extend(block.header.consensus_hash.as_bytes()); + block_header_bytes.extend(block.header.parent_block_id.as_bytes()); + block_header_bytes.extend(block.header.tx_merkle_root.as_bytes()); + block_header_bytes.extend(block.header.state_index_root.as_bytes()); + block_header_bytes.extend(block.header.timestamp.to_be_bytes()); + block_header_bytes.extend(block.header.miner_signature.as_bytes()); + block_header_bytes.extend(block.header.pox_treatment.serialize_to_vec()); + + let hash = Sha512Trunc256Sum::from_data(&block_header_bytes).to_bytes(); + Ok(format!("0x{}", hex::encode(hash))) +} + +#[cfg(feature = "stacks-signers")] +fn get_nakamoto_index_block_hash( + block_hash: &String, + consensus_hash: &clarity::types::chainstate::ConsensusHash, +) -> Result { + use clarity::util::hash::Sha512Trunc256Sum; + + let mut bytes = hex::decode(block_hash[2..].to_string()) + .map_err(|e| format!("unable to decode block hash: {e}"))?; + bytes.extend(consensus_hash.as_bytes()); + + let hash = Sha512Trunc256Sum::from_data(&bytes).to_bytes(); + Ok(format!("0x{}", hex::encode(hash))) } #[cfg(feature = "stacks-signers")] @@ -815,7 +859,7 @@ pub fn get_signer_pubkey_from_stackerdb_chunk_slot( }; let mut digest_bytes = slot.slot_id.to_be_bytes().to_vec(); - digest_bytes.extend(slot.version.to_be_bytes().to_vec()); + digest_bytes.extend(slot.slot_version.to_be_bytes()); let data_bytes_hashed = Sha512Trunc256Sum::from_data(&data_bytes).to_bytes(); digest_bytes.extend(data_bytes_hashed); let digest = Sha512Trunc256Sum::from_data(&digest_bytes).to_bytes(); diff --git a/components/chainhook-sdk/src/indexer/stacks/tests.rs b/components/chainhook-sdk/src/indexer/stacks/tests.rs index e45be1648..e5ae1bf98 100644 --- a/components/chainhook-sdk/src/indexer/stacks/tests.rs +++ b/components/chainhook-sdk/src/indexer/stacks/tests.rs @@ -413,24 +413,24 @@ fn stackerdb_chunks_covert_into_signer_messages() { "01fc3c06f6e0ae5b13c9bb53763661817e55c8e7f1ecab8b4d4b65b283d2dd39f0099e3ea1e25e765f4f0e1dfb0a432309a16a2ec10940e1a14cb9e9b1cbf27edc".to_string(), "010074aff146904763a787aa14c614d0dd1fc63b537bdb2fd351cdf881f6db75f986005eb55250597b25acbf99d3dd3c2fa8189046e1b5d21309a44cbaf2b327c09b0159a01ed3f0094bfa9e5f72f5d894e12ce252081eab5396eb8bba137bddfc365b".to_string() ); - let parsed_chunk = standardize_stacks_stackerdb_chunks(&new_chunks, 1729013425).unwrap(); + let parsed_chunk = standardize_stacks_stackerdb_chunks(&new_chunks).unwrap(); assert_eq!(parsed_chunk.len(), 1); let message = &parsed_chunk[0]; assert_eq!(message.contract, "signers-1-1"); assert_eq!( message.pubkey, - "03c76290f48909b4d49e111d69236a138ce96df3e05f709e425153d99f4fe671b4" + "0x03c76290f48909b4d49e111d69236a138ce96df3e05f709e425153d99f4fe671b4" ); - assert_eq!(message.sig, "01fc3c06f6e0ae5b13c9bb53763661817e55c8e7f1ecab8b4d4b65b283d2dd39f0099e3ea1e25e765f4f0e1dfb0a432309a16a2ec10940e1a14cb9e9b1cbf27edc"); + assert_eq!(message.sig, "0x01fc3c06f6e0ae5b13c9bb53763661817e55c8e7f1ecab8b4d4b65b283d2dd39f0099e3ea1e25e765f4f0e1dfb0a432309a16a2ec10940e1a14cb9e9b1cbf27edc"); match &message.message { StacksSignerMessage::BlockResponse(block_response_data) => match block_response_data { BlockResponseData::Accepted(block_accepted_response) => { - assert_eq!(block_accepted_response.sig, "005eb55250597b25acbf99d3dd3c2fa8189046e1b5d21309a44cbaf2b327c09b0159a01ed3f0094bfa9e5f72f5d894e12ce252081eab5396eb8bba137bddfc365b"); + assert_eq!(block_accepted_response.sig, "0x005eb55250597b25acbf99d3dd3c2fa8189046e1b5d21309a44cbaf2b327c09b0159a01ed3f0094bfa9e5f72f5d894e12ce252081eab5396eb8bba137bddfc365b"); assert_eq!( block_accepted_response.signer_signature_hash, - "74aff146904763a787aa14c614d0dd1fc63b537bdb2fd351cdf881f6db75f986" + "0x74aff146904763a787aa14c614d0dd1fc63b537bdb2fd351cdf881f6db75f986" ); } _ => assert!(false), diff --git a/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs b/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs index e944b986f..6f8386861 100644 --- a/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs +++ b/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs @@ -127,21 +127,24 @@ pub fn create_new_stackerdb_chunk( slot_data: String, ) -> crate::indexer::stacks::NewStackerDbChunks { use crate::indexer::stacks::{ - NewSignerModifiedSlot, NewStackerDbChunkIssuer, NewStackerDbChunksContractId, + NewSignerModifiedSlot, NewStackerDbChunkIssuerId, NewStackerDbChunkIssuerSlots, + NewStackerDbChunksContractId, }; crate::indexer::stacks::NewStackerDbChunks { contract_id: NewStackerDbChunksContractId { name: contract_name, - issuer: vec![NewStackerDbChunkIssuer { - issuer_id: 26, - slots: vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - }], + issuer: ( + NewStackerDbChunkIssuerId(26), + NewStackerDbChunkIssuerSlots(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + ), }, modified_slots: vec![NewSignerModifiedSlot { sig: slot_sig, data: slot_data, slot_id: 1, - version: 141, + slot_version: 141, }], } } diff --git a/components/chainhook-sdk/src/observer/http.rs b/components/chainhook-sdk/src/observer/http.rs index 7468fa4a1..5a5938f51 100644 --- a/components/chainhook-sdk/src/observer/http.rs +++ b/components/chainhook-sdk/src/observer/http.rs @@ -178,8 +178,8 @@ pub fn handle_new_stacks_block( success_response() } -#[cfg(feature = "stacks-signers")] #[post("/stackerdb_chunks", format = "application/json", data = "")] +#[cfg(feature = "stacks-signers")] pub fn handle_stackerdb_chunks( indexer_rw_lock: &State>>, payload: Json, @@ -196,7 +196,7 @@ pub fn handle_stackerdb_chunks( }; let chain_event = match indexer_rw_lock.inner().write() { Ok(mut indexer) => indexer - .handle_stacks_marshalled_stackerdb_chunk(payload.into_inner(), epoch.as_secs(), ctx), + .handle_stacks_marshalled_stackerdb_chunk(payload.into_inner(), epoch.as_millis(), ctx), Err(e) => { return error_response(format!("Unable to acquire background_job_tx: {e}"), ctx); } diff --git a/components/chainhook-sdk/src/observer/mod.rs b/components/chainhook-sdk/src/observer/mod.rs index 0e9a15cd6..038642b4d 100644 --- a/components/chainhook-sdk/src/observer/mod.rs +++ b/components/chainhook-sdk/src/observer/mod.rs @@ -885,7 +885,7 @@ pub fn start_event_observer( pub async fn start_bitcoin_event_observer( config: EventObserverConfig, - observer_commands_tx: Sender, + _observer_commands_tx: Sender, observer_commands_rx: Receiver, observer_events_tx: Option>, observer_sidecar: Option, @@ -897,7 +897,8 @@ pub async fn start_bitcoin_event_observer( let ctx_moved = ctx.clone(); let config_moved = config.clone(); let _ = hiro_system_kit::thread_named("ZMQ handler").spawn(move || { - let future = zmq::start_zeromq_runloop(&config_moved, observer_commands_tx, &ctx_moved); + let future = + zmq::start_zeromq_runloop(&config_moved, _observer_commands_tx, &ctx_moved); hiro_system_kit::nestable_block_on(future); }); } @@ -1658,12 +1659,20 @@ pub async fn start_observer_commands_handler( report.track_expiration(uuid, block_identifier); } for entry in predicates_triggered.iter() { - let blocks_ids = entry + let mut block_ids = entry .apply .iter() .map(|e| e.1.get_identifier()) .collect::>(); - report.track_trigger(&entry.chainhook.uuid, &blocks_ids); + let mut event_block_ids = entry + .events + .iter() + .map(|e| &e.received_at_block) + .collect::>(); + if event_block_ids.len() > 0 { + block_ids.append(&mut event_block_ids); + } + report.track_trigger(&entry.chainhook.uuid, &block_ids); } ctx.try_log(|logger| { slog::info!( diff --git a/components/chainhook-sdk/src/utils/mod.rs b/components/chainhook-sdk/src/utils/mod.rs index 4de1df404..b74f2edbe 100644 --- a/components/chainhook-sdk/src/utils/mod.rs +++ b/components/chainhook-sdk/src/utils/mod.rs @@ -6,8 +6,7 @@ use std::{ }; use chainhook_types::{ - BitcoinBlockData, BlockHeader, BlockIdentifier, StacksBlockData, StacksMicroblockData, - StacksTransactionData, + BitcoinBlockData, BlockHeader, BlockIdentifier, StacksBlockData, StacksMicroblockData, StacksTransactionData }; use hiro_system_kit::slog::{self, Logger}; use reqwest::RequestBuilder; diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index ac75846a3..9acb4c250 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -664,6 +664,19 @@ pub struct BlockchainUpdatedWithReorg { pub confirmed_headers: Vec, } +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(tag = "type", content = "data")] +pub enum StacksNonConsensusEventPayloadData { + SignerMessage(StacksStackerDbChunk), +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct StacksNonConsensusEventData { + pub payload: StacksNonConsensusEventPayloadData, + pub received_at_ms: u64, + pub received_at_block: BlockIdentifier, +} + #[derive(Debug, Clone, PartialEq, Serialize)] pub struct BlockHeader { pub block_identifier: BlockIdentifier, @@ -691,8 +704,8 @@ pub struct BitcoinChainUpdatedWithReorgData { } #[derive(Debug, Clone, PartialEq, Serialize)] -pub struct StacksChainUpdatedWithStackerDbChunksData { - pub chunks: Vec, +pub struct StacksChainUpdatedWithNonConsensusEventsData { + pub events: Vec, } #[allow(dead_code)] @@ -702,7 +715,7 @@ pub enum StacksChainEvent { ChainUpdatedWithReorg(StacksChainUpdatedWithReorgData), ChainUpdatedWithMicroblocks(StacksChainUpdatedWithMicroblocksData), ChainUpdatedWithMicroblocksReorg(StacksChainUpdatedWithMicroblocksReorgData), - ChainUpdatedWithStackerDbChunks(StacksChainUpdatedWithStackerDbChunksData), + ChainUpdatedWithNonConsensusEvents(StacksChainUpdatedWithNonConsensusEventsData), } impl StacksChainEvent { @@ -732,7 +745,7 @@ impl StacksChainEvent { .microblocks_to_apply .first() .and_then(|b| Some(&b.metadata.anchor_block_identifier)), - StacksChainEvent::ChainUpdatedWithStackerDbChunks(_) => None, + StacksChainEvent::ChainUpdatedWithNonConsensusEvents(_) => None, } } } diff --git a/components/chainhook-types-rs/src/signers.rs b/components/chainhook-types-rs/src/signers.rs index 96d1f3ea7..131a32b67 100644 --- a/components/chainhook-types-rs/src/signers.rs +++ b/components/chainhook-types-rs/src/signers.rs @@ -18,12 +18,13 @@ pub struct NakamotoBlockHeaderData { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct NakamotoBlockData { pub header: NakamotoBlockHeaderData, + pub block_hash: String, + pub index_block_hash: String, pub transactions: Vec, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct BlockProposalData { - // TODO(rafaelcr): Include `block_hash` and `index_block_hash`. pub block: NakamotoBlockData, pub burn_height: u64, pub reward_cycle: u64, @@ -93,5 +94,4 @@ pub struct StacksStackerDbChunk { pub sig: String, pub pubkey: String, pub message: StacksSignerMessage, - pub receipt_time: u64, } diff --git a/components/client/typescript/package-lock.json b/components/client/typescript/package-lock.json index 8011fe4b0..55291fdc9 100644 --- a/components/client/typescript/package-lock.json +++ b/components/client/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hirosystems/chainhook-client", - "version": "2.0.0", + "version": "2.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@hirosystems/chainhook-client", - "version": "2.0.0", + "version": "2.1.1", "license": "Apache 2.0", "dependencies": { "@fastify/type-provider-typebox": "^3.2.0", diff --git a/components/client/typescript/package.json b/components/client/typescript/package.json index f93033738..5cb41df87 100644 --- a/components/client/typescript/package.json +++ b/components/client/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/chainhook-client", - "version": "2.0.0", + "version": "2.1.1", "description": "Chainhook TypeScript client", "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/components/client/typescript/src/schemas/stacks/if_this.ts b/components/client/typescript/src/schemas/stacks/if_this.ts index 9f3f709eb..d23689bdb 100644 --- a/components/client/typescript/src/schemas/stacks/if_this.ts +++ b/components/client/typescript/src/schemas/stacks/if_this.ts @@ -74,6 +74,27 @@ export type StacksIfThisContractDeploymentTrait = Static< typeof StacksIfThisContractDeploymentTraitSchema >; +export const StacksIfThisSignerMessageAfterTimestampSchema = Type.Object({ + scope: Type.Literal('signer_message'), + after_timestamp: Type.Integer(), +}); +export type StacksIfThisSignerMessageAfterTimestamp = Static< + typeof StacksIfThisSignerMessageAfterTimestampSchema +>; + +export const StacksIfThisSignerMessageSignerPubKeySchema = Type.Object({ + scope: Type.Literal('signer_message'), + signer_pubkey: Type.String(), +}); +export type StacksIfThisSignerMessageSignerPubKey = Static< + typeof StacksIfThisSignerMessageSignerPubKeySchema +>; + +export const StacksIfThisSignerMessageSchema = Type.Union([ + StacksIfThisSignerMessageAfterTimestampSchema, +]); +export type StacksIfThisSignerMessage = Static; + export const StacksIfThisOptionsSchema = Type.Object({ start_block: Type.Optional(Type.Integer()), end_block: Type.Optional(Type.Integer()), @@ -93,6 +114,7 @@ export const StacksIfThisSchema = Type.Union([ StacksIfThisContractCallSchema, StacksIfThisContractDeploymentSchema, StacksIfThisContractDeploymentTraitSchema, + StacksIfThisSignerMessageSchema, ]); export type StacksIfThis = Static; diff --git a/components/client/typescript/src/schemas/stacks/payload.ts b/components/client/typescript/src/schemas/stacks/payload.ts index ef5969eb3..3bf1a980a 100644 --- a/components/client/typescript/src/schemas/stacks/payload.ts +++ b/components/client/typescript/src/schemas/stacks/payload.ts @@ -102,7 +102,11 @@ export const StacksEventSchema = Type.Object({ }); export type StacksEvent = Static; -export const StacksNonConsensusEventSchema = Type.Union([StacksSignerMessageEventSchema]); +export const StacksNonConsensusEventSchema = Type.Object({ + payload: Type.Union([StacksSignerMessageEventSchema]), + received_at_ms: Type.Integer(), + received_at_block: BlockIdentifierSchema, +}); export type StacksNonConsensusEvent = Static; export const StacksPayloadSchema = Type.Object({ diff --git a/components/client/typescript/src/schemas/stacks/signers.ts b/components/client/typescript/src/schemas/stacks/signers.ts index 701486901..caa0e2e3a 100644 --- a/components/client/typescript/src/schemas/stacks/signers.ts +++ b/components/client/typescript/src/schemas/stacks/signers.ts @@ -1,6 +1,4 @@ import { Static, Type } from '@fastify/type-provider-typebox'; -import { BlockIdentifierSchema } from '../common'; -import { StacksTransactionSchema } from './payload'; export const StacksNakamotoBlockHeaderSchema = Type.Object({ version: Type.Integer(), @@ -19,7 +17,10 @@ export type StacksNakamotoBlockHeader = Static; @@ -94,11 +95,12 @@ export const StacksSignerMessageSchema = Type.Union([ export type StacksSignerMessage = Static; export const StacksSignerMessageEventSchema = Type.Object({ - contract: Type.String(), - sig: Type.String(), - pubkey: Type.String(), - message: StacksSignerMessageSchema, - received_at: Type.Integer(), - received_at_block: BlockIdentifierSchema, + type: Type.Literal('SignerMessage'), + data: Type.Object({ + contract: Type.String(), + sig: Type.String(), + pubkey: Type.String(), + message: StacksSignerMessageSchema, + }), }); export type StacksSignerMessageEvent = Static; diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index 989f3c230..c8f04fdcd 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -1207,6 +1207,48 @@ ] } } + }, + { + "type": "object", + "oneOf": [ + { + "type": "object", + "required": [ + "after_timestamp" + ], + "properties": { + "after_timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "from_signer_pub_key" + ], + "properties": { + "from_signer_pub_key": { + "type": "string" + } + }, + "additionalProperties": false + } + ], + "required": [ + "scope" + ], + "properties": { + "scope": { + "type": "string", + "enum": [ + "signer_message" + ] + } + } } ] },