• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 25404138305-1

05 May 2026 09:47PM UTC coverage: 85.69% (-0.02%) from 85.712%
25404138305-1

Pull #7169

github

497ffd
web-flow
Merge 35db1183d into 53ffba0ab
Pull Request #7169: Feat: add defensive memory allocation for miners/signers

134 of 139 new or added lines in 11 files covered. (96.4%)

4591 existing lines in 96 files now uncovered.

187733 of 219085 relevant lines covered (85.69%)

18687545.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.61
/stackslib/src/util_lib/signed_structured_data.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use clarity::vm::types::TupleData;
18
use clarity::vm::{ClarityName, Value};
19
use stacks_common::types::chainstate::StacksPrivateKey;
20
use stacks_common::types::PrivateKey;
21
use stacks_common::util::hash::Sha256Sum;
22
use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey};
23

24
use crate::chainstate::stacks::address::PoxAddress;
25

26
/// Message prefix for signed structured data. "SIP018" in ascii
27
pub const STRUCTURED_DATA_PREFIX: [u8; 6] = [0x53, 0x49, 0x50, 0x30, 0x31, 0x38];
28

29
pub fn structured_data_hash(value: Value) -> Sha256Sum {
170,129✔
30
    let mut bytes = vec![];
170,129✔
31
    value.serialize_write(&mut bytes).unwrap();
170,129✔
32
    Sha256Sum::from_data(bytes.as_slice())
170,129✔
33
}
170,129✔
34

35
/// Generate a message hash for signing structured Clarity data.
36
/// Reference [SIP018](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md) for more information.
37
pub fn structured_data_message_hash(structured_data: Value, domain: Value) -> Sha256Sum {
85,064✔
38
    let message = [
85,064✔
39
        STRUCTURED_DATA_PREFIX.as_ref(),
85,064✔
40
        structured_data_hash(domain).as_bytes(),
85,064✔
41
        structured_data_hash(structured_data).as_bytes(),
85,064✔
42
    ]
85,064✔
43
    .concat();
85,064✔
44

45
    Sha256Sum::from_data(&message)
85,064✔
46
}
85,064✔
47

48
/// Sign structured Clarity data with a given private key.
49
/// Reference [SIP018](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md) for more information.
50
pub fn sign_structured_data(
1✔
51
    structured_data: Value,
1✔
52
    domain: Value,
1✔
53
    private_key: &Secp256k1PrivateKey,
1✔
54
) -> Result<MessageSignature, &str> {
1✔
55
    let msg_hash = structured_data_message_hash(structured_data, domain);
1✔
56
    private_key.sign(msg_hash.as_bytes())
1✔
57
}
1✔
58

59
// Helper function to generate domain for structured data hash
60
pub fn make_structured_data_domain(name: &str, version: &str, chain_id: u32) -> Value {
85,062✔
61
    Value::Tuple(
85,062✔
62
        TupleData::from_data(vec![
85,062✔
63
            (
85,062✔
64
                ClarityName::from_literal("name"),
85,062✔
65
                Value::string_ascii_from_bytes(name.into()).unwrap(),
85,062✔
66
            ),
85,062✔
67
            (
85,062✔
68
                ClarityName::from_literal("version"),
85,062✔
69
                Value::string_ascii_from_bytes(version.into()).unwrap(),
85,062✔
70
            ),
85,062✔
71
            (
85,062✔
72
                ClarityName::from_literal("chain-id"),
85,062✔
73
                Value::UInt(chain_id.into()),
85,062✔
74
            ),
85,062✔
75
        ])
85,062✔
76
        .unwrap(),
85,062✔
77
    )
85,062✔
78
}
85,062✔
79

80
pub mod pox4 {
81
    use clarity::vm::ClarityName;
82

83
    use super::{
84
        make_structured_data_domain, structured_data_message_hash, MessageSignature, PoxAddress,
85
        PrivateKey, Sha256Sum, StacksPrivateKey, TupleData, Value,
86
    };
87
    define_named_enum!(Pox4SignatureTopic {
88
        StackStx("stack-stx"),
89
        AggregationCommit("agg-commit"),
90
        AggregationIncrease("agg-increase"),
91
        StackExtend("stack-extend"),
92
        StackIncrease("stack-increase"),
93
    });
94

95
    pub fn make_pox_4_signed_data_domain(chain_id: u32) -> Value {
14,452✔
96
        make_structured_data_domain("pox-4-signer", "1.0.0", chain_id)
14,452✔
97
    }
14,452✔
98

99
    #[cfg_attr(test, mutants::skip)]
100
    pub fn make_pox_4_signer_key_message_hash(
14,452✔
101
        pox_addr: &PoxAddress,
14,452✔
102
        reward_cycle: u128,
14,452✔
103
        topic: &Pox4SignatureTopic,
14,452✔
104
        chain_id: u32,
14,452✔
105
        period: u128,
14,452✔
106
        max_amount: u128,
14,452✔
107
        auth_id: u128,
14,452✔
108
    ) -> Sha256Sum {
14,452✔
109
        let domain_tuple = make_pox_4_signed_data_domain(chain_id);
14,452✔
110
        let data_tuple = Value::Tuple(
14,452✔
111
            TupleData::from_data(vec![
14,452✔
112
                (
14,452✔
113
                    ClarityName::from_literal("pox-addr"),
14,452✔
114
                    pox_addr
14,452✔
115
                        .clone()
14,452✔
116
                        .as_clarity_tuple()
14,452✔
117
                        .expect("Error creating signature hash - invalid PoX Address")
14,452✔
118
                        .into(),
14,452✔
119
                ),
14,452✔
120
                (
14,452✔
121
                    ClarityName::from_literal("reward-cycle"),
14,452✔
122
                    Value::UInt(reward_cycle),
14,452✔
123
                ),
14,452✔
124
                (ClarityName::from_literal("period"), Value::UInt(period)),
14,452✔
125
                (
14,452✔
126
                    ClarityName::from_literal("topic"),
14,452✔
127
                    Value::string_ascii_from_bytes(topic.get_name_str().into()).unwrap(),
14,452✔
128
                ),
14,452✔
129
                (ClarityName::from_literal("auth-id"), Value::UInt(auth_id)),
14,452✔
130
                (
14,452✔
131
                    ClarityName::from_literal("max-amount"),
14,452✔
132
                    Value::UInt(max_amount),
14,452✔
133
                ),
14,452✔
134
            ])
14,452✔
135
            .expect("Error creating signature hash"),
14,452✔
136
        );
14,452✔
137
        structured_data_message_hash(data_tuple, domain_tuple)
14,452✔
138
    }
14,452✔
139

140
    impl Into<Pox4SignatureTopic> for &'static str {
141
        #[cfg_attr(test, mutants::skip)]
UNCOV
142
        fn into(self) -> Pox4SignatureTopic {
×
UNCOV
143
            match self {
×
UNCOV
144
                "stack-stx" => Pox4SignatureTopic::StackStx,
×
UNCOV
145
                "agg-commit" => Pox4SignatureTopic::AggregationCommit,
×
UNCOV
146
                "stack-extend" => Pox4SignatureTopic::StackExtend,
×
UNCOV
147
                "stack-increase" => Pox4SignatureTopic::StackIncrease,
×
UNCOV
148
                _ => panic!("Invalid pox-4 signature topic"),
×
149
            }
UNCOV
150
        }
×
151
    }
152

153
    #[cfg_attr(test, mutants::skip)]
154
    pub fn make_pox_4_signer_key_signature(
14,360✔
155
        pox_addr: &PoxAddress,
14,360✔
156
        signer_key: &StacksPrivateKey,
14,360✔
157
        reward_cycle: u128,
14,360✔
158
        topic: &Pox4SignatureTopic,
14,360✔
159
        chain_id: u32,
14,360✔
160
        period: u128,
14,360✔
161
        max_amount: u128,
14,360✔
162
        auth_id: u128,
14,360✔
163
    ) -> Result<MessageSignature, &'static str> {
14,360✔
164
        let msg_hash = make_pox_4_signer_key_message_hash(
14,360✔
165
            pox_addr,
14,360✔
166
            reward_cycle,
14,360✔
167
            topic,
14,360✔
168
            chain_id,
14,360✔
169
            period,
14,360✔
170
            max_amount,
14,360✔
171
            auth_id,
14,360✔
172
        );
173
        signer_key.sign(msg_hash.as_bytes())
14,360✔
174
    }
14,360✔
175

176
    #[cfg(test)]
177
    mod tests {
178
        use clarity::vm::clarity::{ClarityConnection, TransactionConnection};
179
        use clarity::vm::costs::LimitedCostTracker;
180
        use clarity::vm::types::PrincipalData;
181
        use clarity::vm::ClarityVersion;
182
        use stacks_common::address::AddressHashMode;
183
        use stacks_common::consts::CHAIN_ID_TESTNET;
184
        use stacks_common::types::chainstate::StacksAddress;
185
        use stacks_common::util::hash::to_hex;
186
        use stacks_common::util::secp256k1::Secp256k1PublicKey;
187

188
        use super::*;
189
        use crate::chainstate::stacks::boot::contract_tests::ClarityTestSim;
190
        use crate::chainstate::stacks::boot::{POX_4_CODE, POX_4_NAME};
191
        use crate::util_lib::boot::boot_code_id;
192

193
        fn call_get_signer_message_hash(
7✔
194
            sim: &mut ClarityTestSim,
7✔
195
            pox_addr: &PoxAddress,
7✔
196
            reward_cycle: u128,
7✔
197
            topic: &Pox4SignatureTopic,
7✔
198
            lock_period: u128,
7✔
199
            sender: &PrincipalData,
7✔
200
            max_amount: u128,
7✔
201
            auth_id: u128,
7✔
202
        ) -> Vec<u8> {
7✔
203
            let pox_contract_id = boot_code_id(POX_4_NAME, false);
7✔
204
            sim.execute_next_block_as_conn(|conn| {
7✔
205
                let result = conn.with_readonly_clarity_env(
7✔
206
                    false,
207
                    CHAIN_ID_TESTNET,
208
                    sender.clone(),
7✔
209
                    None,
7✔
210
                    LimitedCostTracker::new_free(),
7✔
211
                    |exec_state, invoke_ctx| {
7✔
212
                        let program = format!(
7✔
213
                            "(get-signer-key-message-hash {} u{} \"{}\" u{} u{} u{})",
214
                            Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), //p
7✔
215
                            reward_cycle,
216
                            topic.get_name_str(),
7✔
217
                            lock_period,
218
                            max_amount,
219
                            auth_id,
220
                        );
221
                        exec_state.eval_read_only(invoke_ctx, &pox_contract_id, &program)
7✔
222
                    },
7✔
223
                );
224
                result
7✔
225
                    .expect("FATAL: failed to execute contract call")
7✔
226
                    .expect_buff(32)
7✔
227
                    .expect("FATAL: expected buff result")
7✔
228
            })
7✔
229
        }
7✔
230

231
        #[test]
232
        fn test_make_pox_4_message_hash() {
1✔
233
            let mut sim = ClarityTestSim::new();
1✔
234
            sim.epoch_bounds = vec![0, 1, 2];
1✔
235

236
            // Test setup
237
            sim.execute_next_block(|_env| {});
1✔
238
            sim.execute_next_block(|_env| {});
1✔
239
            sim.execute_next_block(|_env| {});
1✔
240

241
            let body = &*POX_4_CODE;
1✔
242
            let pox_contract_id = boot_code_id(POX_4_NAME, false);
1✔
243

244
            sim.execute_next_block_as_conn(|conn| {
1✔
245
                conn.as_transaction(|clarity_db| {
1✔
246
                    let clarity_version = ClarityVersion::Clarity2;
1✔
247
                    let (ast, analysis) = clarity_db
1✔
248
                        .analyze_smart_contract(&pox_contract_id, clarity_version, body)
1✔
249
                        .unwrap();
1✔
250
                    clarity_db
1✔
251
                        .initialize_smart_contract(
1✔
252
                            &pox_contract_id,
1✔
253
                            clarity_version,
1✔
254
                            &ast,
1✔
255
                            body,
1✔
256
                            None,
1✔
257
                            |_, _| None,
258
                            None,
1✔
259
                        )
260
                        .unwrap();
1✔
261
                    clarity_db
1✔
262
                        .save_analysis(&pox_contract_id, &analysis)
1✔
263
                        .expect("FATAL: failed to store contract analysis");
1✔
264
                });
1✔
265
            });
1✔
266

267
            let pubkey = Secp256k1PublicKey::new();
1✔
268
            let stacks_addr = StacksAddress::p2pkh(false, &pubkey);
1✔
269
            let pubkey = Secp256k1PublicKey::new();
1✔
270
            let principal = PrincipalData::from(stacks_addr.clone());
1✔
271
            let pox_addr = PoxAddress::standard_burn_address(false);
1✔
272
            let reward_cycle: u128 = 1;
1✔
273
            let topic = Pox4SignatureTopic::StackStx;
1✔
274
            let lock_period = 12;
1✔
275
            let auth_id = 111;
1✔
276
            let max_amount = u128::MAX;
1✔
277

278
            let expected_hash_vec = make_pox_4_signer_key_message_hash(
1✔
279
                &pox_addr,
1✔
280
                reward_cycle,
1✔
281
                &Pox4SignatureTopic::StackStx,
1✔
282
                CHAIN_ID_TESTNET,
283
                lock_period,
1✔
284
                max_amount,
1✔
285
                auth_id,
1✔
286
            );
287
            let expected_hash = expected_hash_vec.as_bytes();
1✔
288

289
            // Test 1: valid result
290

291
            let result = call_get_signer_message_hash(
1✔
292
                &mut sim,
1✔
293
                &pox_addr,
1✔
294
                reward_cycle,
1✔
295
                &topic,
1✔
296
                lock_period,
1✔
297
                &principal,
1✔
298
                max_amount,
1✔
299
                auth_id,
1✔
300
            );
301
            assert_eq!(expected_hash.clone(), result.as_slice());
1✔
302

303
            // Test 2: invalid pox address
304
            let other_pox_address = PoxAddress::from_legacy(
1✔
305
                AddressHashMode::SerializeP2PKH,
1✔
306
                StacksAddress::p2pkh(false, &Secp256k1PublicKey::new())
1✔
307
                    .destruct()
1✔
308
                    .1,
1✔
309
            );
310
            let result = call_get_signer_message_hash(
1✔
311
                &mut sim,
1✔
312
                &other_pox_address,
1✔
313
                reward_cycle,
1✔
314
                &topic,
1✔
315
                lock_period,
1✔
316
                &principal,
1✔
317
                max_amount,
1✔
318
                auth_id,
1✔
319
            );
320
            assert_ne!(expected_hash.clone(), result.as_slice());
1✔
321

322
            // Test 3: invalid reward cycle
323
            let result = call_get_signer_message_hash(
1✔
324
                &mut sim,
1✔
325
                &pox_addr,
1✔
326
                0,
327
                &topic,
1✔
328
                lock_period,
1✔
329
                &principal,
1✔
330
                max_amount,
1✔
331
                auth_id,
1✔
332
            );
333
            assert_ne!(expected_hash.clone(), result.as_slice());
1✔
334

335
            // Test 4: invalid topic
336
            let result = call_get_signer_message_hash(
1✔
337
                &mut sim,
1✔
338
                &pox_addr,
1✔
339
                reward_cycle,
1✔
340
                &Pox4SignatureTopic::AggregationCommit,
1✔
341
                lock_period,
1✔
342
                &principal,
1✔
343
                max_amount,
1✔
344
                auth_id,
1✔
345
            );
346
            assert_ne!(expected_hash.clone(), result.as_slice());
1✔
347

348
            // Test 5: invalid lock period
349
            let result = call_get_signer_message_hash(
1✔
350
                &mut sim,
1✔
351
                &pox_addr,
1✔
352
                reward_cycle,
1✔
353
                &topic,
1✔
354
                0,
355
                &principal,
1✔
356
                max_amount,
1✔
357
                auth_id,
1✔
358
            );
359
            assert_ne!(expected_hash.clone(), result.as_slice());
1✔
360

361
            // Test 5: invalid max amount
362
            let result = call_get_signer_message_hash(
1✔
363
                &mut sim,
1✔
364
                &pox_addr,
1✔
365
                reward_cycle,
1✔
366
                &topic,
1✔
367
                lock_period,
1✔
368
                &principal,
1✔
369
                1010101,
370
                auth_id,
1✔
371
            );
372
            assert_ne!(expected_hash.clone(), result.as_slice());
1✔
373

374
            // Test 6: invalid auth id
375
            let result = call_get_signer_message_hash(
1✔
376
                &mut sim,
1✔
377
                &pox_addr,
1✔
378
                reward_cycle,
1✔
379
                &topic,
1✔
380
                lock_period,
1✔
381
                &principal,
1✔
382
                max_amount,
1✔
383
                10101,
384
            );
385
            assert_ne!(expected_hash.clone(), result.as_slice());
1✔
386
        }
1✔
387

388
        #[test]
389
        /// Fixture message hash to test against in other libraries
390
        fn test_sig_hash_fixture() {
1✔
391
            let fixture = "ec5b88aa81a96a6983c26cdba537a13d253425348ffc0ba6b07130869b025a2d";
1✔
392
            let pox_addr = PoxAddress::standard_burn_address(false);
1✔
393
            let pubkey_hex = "0206952cd8813a64f7b97144c984015490a8f9c5778e8f928fbc8aa6cbf02f48e6";
1✔
394
            let pubkey = Secp256k1PublicKey::from_hex(pubkey_hex).unwrap();
1✔
395
            let reward_cycle: u128 = 1;
1✔
396
            let lock_period = 12;
1✔
397
            let auth_id = 111;
1✔
398
            let max_amount = u128::MAX;
1✔
399

400
            let message_hash = make_pox_4_signer_key_message_hash(
1✔
401
                &pox_addr,
1✔
402
                reward_cycle,
1✔
403
                &Pox4SignatureTopic::StackStx,
1✔
404
                CHAIN_ID_TESTNET,
405
                lock_period,
1✔
406
                max_amount,
1✔
407
                auth_id,
1✔
408
            );
409

410
            assert_eq!(to_hex(message_hash.as_bytes()), fixture);
1✔
411
        }
1✔
412
    }
413
}
414

415
#[cfg(test)]
416
mod test {
417
    use clarity::vm::types::{TupleData, Value};
418
    use stacks_common::consts::CHAIN_ID_MAINNET;
419
    use stacks_common::util::hash::to_hex;
420

421
    use super::*;
422

423
    /// [SIP18 test vectors](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md)
424
    #[test]
425
    fn test_sip18_ref_structured_data_hash() {
1✔
426
        let value = Value::string_ascii_from_bytes("Hello World".into()).unwrap();
1✔
427
        let msg_hash = structured_data_hash(value);
1✔
428
        assert_eq!(
1✔
429
            to_hex(msg_hash.as_bytes()),
1✔
430
            "5297eef9765c466d945ad1cb2c81b30b9fed6c165575dc9226e9edf78b8cd9e8"
431
        )
432
    }
1✔
433

434
    /// [SIP18 test vectors](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md)
435
    #[test]
436
    fn test_sip18_ref_message_hashing() {
1✔
437
        let domain = Value::Tuple(
1✔
438
            TupleData::from_data(vec![
1✔
439
                (
1✔
440
                    ClarityName::from_literal("name"),
1✔
441
                    Value::string_ascii_from_bytes("Test App".into()).unwrap(),
1✔
442
                ),
1✔
443
                (
1✔
444
                    ClarityName::from_literal("version"),
1✔
445
                    Value::string_ascii_from_bytes("1.0.0".into()).unwrap(),
1✔
446
                ),
1✔
447
                (
1✔
448
                    ClarityName::from_literal("chain-id"),
1✔
449
                    Value::UInt(CHAIN_ID_MAINNET.into()),
1✔
450
                ),
1✔
451
            ])
1✔
452
            .unwrap(),
1✔
453
        );
1✔
454
        let data = Value::string_ascii_from_bytes("Hello World".into()).unwrap();
1✔
455

456
        let msg_hash = structured_data_message_hash(data, domain);
1✔
457

458
        assert_eq!(
1✔
459
            to_hex(msg_hash.as_bytes()),
1✔
460
            "1bfdab6d4158313ce34073fbb8d6b0fc32c154d439def12247a0f44bb2225259"
461
        );
462
    }
1✔
463

464
    /// [SIP18 test vectors](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md)
465
    #[test]
466
    fn test_sip18_ref_signing() {
1✔
467
        let key = Secp256k1PrivateKey::from_hex(
1✔
468
            "753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601",
1✔
469
        )
470
        .unwrap();
1✔
471
        let domain = Value::Tuple(
1✔
472
            TupleData::from_data(vec![
1✔
473
                (
1✔
474
                    ClarityName::from_literal("name"),
1✔
475
                    Value::string_ascii_from_bytes("Test App".into()).unwrap(),
1✔
476
                ),
1✔
477
                (
1✔
478
                    ClarityName::from_literal("version"),
1✔
479
                    Value::string_ascii_from_bytes("1.0.0".into()).unwrap(),
1✔
480
                ),
1✔
481
                (
1✔
482
                    ClarityName::from_literal("chain-id"),
1✔
483
                    Value::UInt(CHAIN_ID_MAINNET.into()),
1✔
484
                ),
1✔
485
            ])
1✔
486
            .unwrap(),
1✔
487
        );
1✔
488
        let data = Value::string_ascii_from_bytes("Hello World".into()).unwrap();
1✔
489
        let signature =
1✔
490
            sign_structured_data(data, domain, &key).expect("Failed to sign structured data");
1✔
491

492
        let signature_rsv = signature.to_rsv();
1✔
493

494
        assert_eq!(to_hex(signature_rsv.as_slice()), "8b94e45701d857c9f1d1d70e8b2ca076045dae4920fb0160be0642a68cd78de072ab527b5c5277a593baeb2a8b657c216b99f7abb5d14af35b4bf12ba6460ba401");
1✔
495
    }
1✔
496

497
    #[test]
498
    fn test_prefix_bytes() {
1✔
499
        let hex = to_hex(STRUCTURED_DATA_PREFIX.as_ref());
1✔
500
        assert_eq!(hex, "534950303138");
1✔
501
    }
1✔
502
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc