ckb_sdk/
util.rs

1use std::{convert::TryInto, ptr, sync::atomic};
2
3use ckb_dao_utils::extract_dao_data;
4use ckb_types::{
5    core::{Capacity, EpochNumber, EpochNumberWithFraction, HeaderView},
6    packed::CellOutput,
7    prelude::*,
8    H160, H256, U256,
9};
10use sha3::{Digest, Keccak256};
11
12#[cfg(not(target_arch = "wasm32"))]
13use crate::rpc::CkbRpcClient;
14
15use crate::rpc::CkbRpcAsyncClient;
16use crate::traits::LiveCell;
17
18use secp256k1::ffi::CPtr;
19
20pub fn zeroize_privkey(key: &mut secp256k1::SecretKey) {
21    let key_ptr = key.as_mut_c_ptr();
22    for i in 0..key.as_ref().len() as isize {
23        unsafe { ptr::write_volatile(key_ptr.offset(i), Default::default()) }
24        atomic::compiler_fence(atomic::Ordering::SeqCst);
25    }
26}
27
28pub fn zeroize_slice(data: &mut [u8]) {
29    for elem in data {
30        unsafe { ptr::write_volatile(elem, Default::default()) }
31        atomic::compiler_fence(atomic::Ordering::SeqCst);
32    }
33}
34#[cfg(not(target_arch = "wasm32"))]
35pub fn get_max_mature_number(rpc_client: &CkbRpcClient) -> Result<u64, String> {
36    crate::rpc::block_on(get_max_mature_number_async(&rpc_client.into()))
37}
38
39pub async fn get_max_mature_number_async(rpc_client: &CkbRpcAsyncClient) -> Result<u64, String> {
40    let cellbase_maturity = EpochNumberWithFraction::from_full_value(
41        rpc_client
42            .get_consensus()
43            .await
44            .map_err(|err| err.to_string())?
45            .cellbase_maturity
46            .value(),
47    );
48    let tip_epoch = rpc_client
49        .get_tip_header()
50        .await
51        .map(|header| EpochNumberWithFraction::from_full_value(header.inner.epoch.value()))
52        .map_err(|err| err.to_string())?;
53
54    let tip_epoch_rational = tip_epoch.to_rational();
55    let cellbase_maturity_rational = cellbase_maturity.to_rational();
56
57    if tip_epoch_rational < cellbase_maturity_rational {
58        // No cellbase live cell is mature
59        Ok(0)
60    } else {
61        let difference = tip_epoch_rational - cellbase_maturity_rational;
62        let rounds_down_difference = difference.clone().into_u256();
63        let difference_delta = difference - rounds_down_difference.clone();
64
65        let epoch_number = u64::from_le_bytes(
66            rounds_down_difference.to_le_bytes()[..8]
67                .try_into()
68                .expect("should be u64"),
69        )
70        .into();
71        let max_mature_epoch = rpc_client
72            .get_epoch_by_number(epoch_number)
73            .await
74            .map_err(|err| err.to_string())?
75            .ok_or_else(|| "Can not get epoch less than current epoch number".to_string())?;
76
77        let max_mature_block_number = (difference_delta
78            * U256::from(max_mature_epoch.length.value())
79            + U256::from(max_mature_epoch.start_number.value()))
80        .into_u256();
81
82        Ok(u64::from_le_bytes(
83            max_mature_block_number.to_le_bytes()[..8]
84                .try_into()
85                .expect("should be u64"),
86        ))
87    }
88}
89
90pub fn is_mature(info: &LiveCell, max_mature_number: u64) -> bool {
91    // Not cellbase cell
92    info.tx_index > 0
93    // Live cells in genesis are all mature
94        || info.block_number == 0
95        || info.block_number <= max_mature_number
96}
97
98pub fn minimal_unlock_point(
99    deposit_header: &HeaderView,
100    prepare_header: &HeaderView,
101) -> EpochNumberWithFraction {
102    const LOCK_PERIOD_EPOCHES: EpochNumber = 180;
103
104    // https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/dao.c#L182-L223
105    let deposit_point = deposit_header.epoch();
106    let prepare_point = prepare_header.epoch();
107    let prepare_fraction = prepare_point.index() * deposit_point.length();
108    let deposit_fraction = deposit_point.index() * prepare_point.length();
109    let passed_epoch_cnt = if prepare_fraction > deposit_fraction {
110        prepare_point.number() - deposit_point.number() + 1
111    } else {
112        prepare_point.number() - deposit_point.number()
113    };
114    let rest_epoch_cnt = passed_epoch_cnt.div_ceil(LOCK_PERIOD_EPOCHES) * LOCK_PERIOD_EPOCHES;
115    EpochNumberWithFraction::new(
116        deposit_point.number() + rest_epoch_cnt,
117        deposit_point.index(),
118        deposit_point.length(),
119    )
120}
121
122pub fn calculate_dao_maximum_withdraw4(
123    deposit_header: &HeaderView,
124    prepare_header: &HeaderView,
125    output: &CellOutput,
126    occupied_capacity: u64,
127) -> u64 {
128    let (deposit_ar, _, _, _) = extract_dao_data(deposit_header.dao());
129    let (prepare_ar, _, _, _) = extract_dao_data(prepare_header.dao());
130    let output_capacity: Capacity = output.capacity().unpack();
131    let counted_capacity = output_capacity.as_u64() - occupied_capacity;
132    let withdraw_counted_capacity =
133        u128::from(counted_capacity) * u128::from(prepare_ar) / u128::from(deposit_ar);
134    occupied_capacity + withdraw_counted_capacity as u64
135}
136
137pub fn serialize_signature(signature: &secp256k1::ecdsa::RecoverableSignature) -> [u8; 65] {
138    let (recov_id, data) = signature.serialize_compact();
139    let mut signature_bytes = [0u8; 65];
140    signature_bytes[0..64].copy_from_slice(&data[0..64]);
141    signature_bytes[64] = i32::from(recov_id) as u8;
142    signature_bytes
143}
144
145pub fn blake160(message: &[u8]) -> H160 {
146    let r = ckb_hash::blake2b_256(message);
147    H160::from_slice(&r[..20]).unwrap()
148}
149
150/// Do an ethereum style public key hash.
151pub fn keccak160(message: &[u8]) -> H160 {
152    let mut hasher = Keccak256::new();
153    hasher.update(message);
154    let r = hasher.finalize();
155    H160::from_slice(&r[12..]).unwrap()
156}
157
158/// Do an ethereum style message convert before do a signature.
159pub fn convert_keccak256_hash(message: &[u8]) -> H256 {
160    let eth_prefix: &[u8; 28] = b"\x19Ethereum Signed Message:\n32";
161    let mut hasher = Keccak256::new();
162    hasher.update(eth_prefix);
163    hasher.update(message);
164    let r = hasher.finalize();
165    H256::from_slice(&r).expect("convert_keccak256_hash")
166}
167
168#[cfg(all(test, feature = "test"))]
169mod tests {
170    use super::*;
171    use crate::test_util::MockRpcResult;
172    use ckb_chain_spec::consensus::ConsensusBuilder;
173    use ckb_dao_utils::pack_dao_data;
174    use ckb_jsonrpc_types::{Consensus, EpochView, HeaderView};
175    use ckb_types::{
176        bytes::Bytes,
177        core::{capacity_bytes, EpochNumberWithFraction, HeaderBuilder},
178    };
179    use httpmock::prelude::*;
180
181    #[test]
182    fn test_minimal_unlock_point() {
183        let cases = vec![
184            ((5, 5, 1000), (184, 4, 1000), (5 + 180, 5, 1000)),
185            ((5, 5, 1000), (184, 5, 1000), (5 + 180, 5, 1000)),
186            ((5, 5, 1000), (184, 6, 1000), (5 + 180, 5, 1000)),
187            ((5, 5, 1000), (185, 4, 1000), (5 + 180, 5, 1000)),
188            ((5, 5, 1000), (185, 5, 1000), (5 + 180, 5, 1000)),
189            ((5, 5, 1000), (185, 6, 1000), (5 + 180 * 2, 5, 1000)), // 6/1000 > 5/1000
190            ((5, 5, 1000), (186, 4, 1000), (5 + 180 * 2, 5, 1000)),
191            ((5, 5, 1000), (186, 5, 1000), (5 + 180 * 2, 5, 1000)),
192            ((5, 5, 1000), (186, 6, 1000), (5 + 180 * 2, 5, 1000)),
193            ((5, 5, 1000), (364, 4, 1000), (5 + 180 * 2, 5, 1000)),
194            ((5, 5, 1000), (364, 5, 1000), (5 + 180 * 2, 5, 1000)),
195            ((5, 5, 1000), (364, 6, 1000), (5 + 180 * 2, 5, 1000)),
196            ((5, 5, 1000), (365, 4, 1000), (5 + 180 * 2, 5, 1000)),
197            ((5, 5, 1000), (365, 5, 1000), (5 + 180 * 2, 5, 1000)),
198            ((5, 5, 1000), (365, 6, 1000), (5 + 180 * 3, 5, 1000)),
199            ((5, 5, 1000), (366, 4, 1000), (5 + 180 * 3, 5, 1000)),
200            ((5, 5, 1000), (366, 5, 1000), (5 + 180 * 3, 5, 1000)),
201            ((5, 5, 1000), (366, 6, 1000), (5 + 180 * 3, 5, 1000)),
202        ];
203        for (deposit_point, prepare_point, expected) in cases {
204            let deposit_point =
205                EpochNumberWithFraction::new(deposit_point.0, deposit_point.1, deposit_point.2);
206            let prepare_point =
207                EpochNumberWithFraction::new(prepare_point.0, prepare_point.1, prepare_point.2);
208            let expected = EpochNumberWithFraction::new(expected.0, expected.1, expected.2);
209            let deposit_header = HeaderBuilder::default()
210                .epoch(deposit_point.full_value())
211                .build();
212            let prepare_header = HeaderBuilder::default()
213                .epoch(prepare_point.full_value())
214                .build();
215            let actual = minimal_unlock_point(&deposit_header, &prepare_header);
216            assert_eq!(
217                expected, actual,
218                "minimal_unlock_point deposit_point: {}, prepare_point: {}, expected: {}, actual: {}",
219                deposit_point, prepare_point, expected, actual,
220            );
221        }
222    }
223
224    #[test]
225    fn check_withdraw_calculation() {
226        let data = Bytes::from(vec![1; 10]);
227        let output = CellOutput::new_builder()
228            .capacity(capacity_bytes!(1000000).pack())
229            .build();
230
231        let (deposit_point, prepare_point) = ((5, 5, 1000), (184, 4, 1000));
232        let deposit_number = deposit_point.0 * deposit_point.2 + deposit_point.1;
233        let prepare_number = prepare_point.0 * prepare_point.2 + prepare_point.1;
234        let deposit_point =
235            EpochNumberWithFraction::new(deposit_point.0, deposit_point.1, deposit_point.2);
236        let prepare_point =
237            EpochNumberWithFraction::new(prepare_point.0, prepare_point.1, prepare_point.2);
238        let deposit_header = HeaderBuilder::default()
239            .epoch(deposit_point.full_value())
240            .number(deposit_number)
241            .dao(pack_dao_data(
242                10_000_000_000_123_456,
243                Default::default(),
244                Default::default(),
245                Default::default(),
246            ))
247            .build();
248        let prepare_header = HeaderBuilder::default()
249            .epoch(prepare_point.full_value())
250            .number(prepare_number)
251            .dao(pack_dao_data(
252                10_000_000_001_123_456,
253                Default::default(),
254                Default::default(),
255                Default::default(),
256            ))
257            .build();
258
259        let result = calculate_dao_maximum_withdraw4(
260            &deposit_header,
261            &prepare_header,
262            &output,
263            Capacity::bytes(data.len()).unwrap().as_u64(),
264        );
265        assert_eq!(result, 100_000_000_009_999);
266    }
267
268    #[test]
269    fn test_get_max_mature_number() {
270        {
271            // cellbase maturity is 4, tip epoch is 3(200/400), so the max mature block number is 0
272            let server = MockServer::start();
273            let consensus: Consensus = ConsensusBuilder::default()
274                .cellbase_maturity(EpochNumberWithFraction::new(4, 0, 1))
275                .build()
276                .into();
277            server.mock(|when, then| {
278                when.method(POST).path("/").body_contains("get_consensus");
279                then.status(200)
280                    .body(MockRpcResult::new(consensus).to_json());
281            });
282
283            let tip_header: HeaderView = HeaderBuilder::default()
284                .epoch(EpochNumberWithFraction::new(3, 200, 400).full_value())
285                .build()
286                .into();
287            server.mock(|when, then| {
288                when.method(POST).path("/").body_contains("get_tip_header");
289                then.status(200)
290                    .body(MockRpcResult::new(tip_header).to_json());
291            });
292
293            let rpc_client = CkbRpcClient::new(server.base_url().as_str());
294            assert_eq!(0, get_max_mature_number(&rpc_client).unwrap());
295        }
296
297        {
298            // cellbase maturity is 3(1/3), tip epoch is 3(300/600), epoch 3 starts at block 1800
299            // so the max mature block number is 1800 + (600 * 1 / 6) = 1900
300            let server = MockServer::start();
301            let consensus: Consensus = ConsensusBuilder::default()
302                .cellbase_maturity(EpochNumberWithFraction::new(3, 1, 3))
303                .build()
304                .into();
305            server.mock(|when, then| {
306                when.method(POST).path("/").body_contains("get_consensus");
307                then.status(200)
308                    .body(MockRpcResult::new(consensus).to_json());
309            });
310
311            let tip_header: HeaderView = HeaderBuilder::default()
312                .epoch(EpochNumberWithFraction::new(3, 300, 600).full_value())
313                .build()
314                .into();
315            server.mock(|when, then| {
316                when.method(POST).path("/").body_contains("get_tip_header");
317                then.status(200)
318                    .body(MockRpcResult::new(tip_header).to_json());
319            });
320
321            let epoch3: EpochView = EpochView {
322                number: 3.into(),
323                start_number: 1800.into(),
324                length: 600.into(),
325                compact_target: 0.into(),
326            };
327
328            server.mock(|when, then| {
329                when.method(POST)
330                    .path("/")
331                    .body_contains("get_epoch_by_number");
332                then.status(200).body(MockRpcResult::new(epoch3).to_json());
333            });
334
335            let rpc_client = CkbRpcClient::new(server.base_url().as_str());
336            assert_eq!(1900, get_max_mature_number(&rpc_client).unwrap());
337        }
338
339        {
340            // cellbase maturity is 3(2/3), tip epoch is 105(300/600), epoch 101 starts at block 150000 and length is 1800
341            // so the max mature block number is 150000 + (1800 * 5 / 6) = 151500
342            let server = MockServer::start();
343            let consensus: Consensus = ConsensusBuilder::default()
344                .cellbase_maturity(EpochNumberWithFraction::new(3, 2, 3))
345                .build()
346                .into();
347            server.mock(|when, then| {
348                when.method(POST).path("/").body_contains("get_consensus");
349                then.status(200)
350                    .body(MockRpcResult::new(consensus).to_json());
351            });
352
353            let tip_header: HeaderView = HeaderBuilder::default()
354                .epoch(EpochNumberWithFraction::new(105, 300, 600).full_value())
355                .build()
356                .into();
357            server.mock(|when, then| {
358                when.method(POST).path("/").body_contains("get_tip_header");
359                then.status(200)
360                    .body(MockRpcResult::new(tip_header).to_json());
361            });
362
363            let epoch3: EpochView = EpochView {
364                number: 101.into(),
365                start_number: 150000.into(),
366                length: 1800.into(),
367                compact_target: 0.into(),
368            };
369
370            server.mock(|when, then| {
371                when.method(POST)
372                    .path("/")
373                    .body_contains("get_epoch_by_number");
374                then.status(200).body(MockRpcResult::new(epoch3).to_json());
375            });
376
377            let rpc_client = CkbRpcClient::new(server.base_url().as_str());
378            assert_eq!(151500, get_max_mature_number(&rpc_client).unwrap());
379        }
380    }
381}