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 88385368..f4e064e5 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/tests/fixtures/stacks/testnet/occurrence.json b/components/chainhook-sdk/src/chainhooks/tests/fixtures/stacks/testnet/occurrence.json index d1f9474b..51ce27c9 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 1d6a4f84..7478dddd 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 { @@ -848,6 +872,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/tests/helpers/stacks_blocks.rs b/components/chainhook-sdk/src/indexer/tests/helpers/stacks_blocks.rs index 71123b9f..fcbd1e79 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 8f6251d3..5c51c530 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 ee9066b9..6af1972b 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/client/typescript/src/schemas/stacks/payload.ts b/components/client/typescript/src/schemas/stacks/payload.ts index e25974bf..b8768316 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()),