grafos_std/
block.rs

1//! Safe wrappers for FBBU (Fabric Bootstrap Binding Unit) host functions.
2//!
3//! This module provides block-level fabric storage access through the
4//! FBBU data-plane protocol. All I/O operates on fixed 512-byte blocks
5//! addressed by logical block address (LBA).
6//!
7//! # Usage
8//!
9//! ```rust
10//! use grafos_std::block::{FabricBlock, BLOCK_SIZE};
11//!
12//! # grafos_std::host::reset_mock();
13//! # grafos_std::host::mock_set_fbbu_num_blocks(128);
14//! let blk = FabricBlock::hello()?;
15//! assert_eq!(blk.num_blocks(), 128);
16//!
17//! let mut data = [0u8; BLOCK_SIZE];
18//! data[0..5].copy_from_slice(b"hello");
19//! blk.write_block(0, &data)?;
20//!
21//! let readback = blk.read_block(0)?;
22//! assert_eq!(&readback[0..5], b"hello");
23//! # Ok::<(), grafos_std::FabricError>(())
24//! ```
25//!
26//! For capacity-checked allocation, use [`BlockBuilder`]:
27//!
28//! ```rust
29//! use grafos_std::block::BlockBuilder;
30//!
31//! # grafos_std::host::reset_mock();
32//! # grafos_std::host::mock_set_fbbu_num_blocks(256);
33//! let lease = BlockBuilder::new().min_blocks(64).acquire()?;
34//! assert!(lease.block().num_blocks() >= 64);
35//! # Ok::<(), grafos_std::FabricError>(())
36//! ```
37
38extern crate alloc;
39use alloc::vec;
40use alloc::vec::Vec;
41
42use crate::error::{FabricError, Result};
43use crate::host;
44use crate::lease::{self, LeaseInfo, LeaseStatus, SharedLeaseState};
45
46const LEASE_TAG_BLOCK: u8 = 0x02;
47
48fn lease_status_from_host(code: u8) -> LeaseStatus {
49    match code {
50        host::LEASE_STATUS_ACTIVE => LeaseStatus::Active,
51        host::LEASE_STATUS_EXPIRED => LeaseStatus::Expired,
52        host::LEASE_STATUS_REVOKED => LeaseStatus::Revoked,
53        _ => LeaseStatus::Revoked,
54    }
55}
56
57fn sync_state_from_host(state: &SharedLeaseState, info: &host::FbbuLeaseInfo) {
58    lease::set_expires_at_unix_secs(state, info.expires_at_unix_secs);
59    lease::set_status(state, lease_status_from_host(info.status));
60}
61
62/// Block size in bytes (512).
63///
64/// All FBBU I/O operates on fixed-size blocks of this many bytes. This
65/// matches the standard sector size used by the FBBU protocol.
66pub const BLOCK_SIZE: usize = 512;
67
68/// Safe handle to fabric block storage via FBBU host functions.
69///
70/// Created by [`FabricBlock::hello`], which performs the FBBU HELLO
71/// handshake and queries the device capacity. Once created, the handle
72/// can read and write fixed-size 512-byte blocks by LBA.
73///
74/// # Examples
75///
76/// ```rust
77/// use grafos_std::block::{FabricBlock, BLOCK_SIZE};
78///
79/// # grafos_std::host::reset_mock();
80/// # grafos_std::host::mock_set_fbbu_num_blocks(64);
81/// let blk = FabricBlock::hello()?;
82///
83/// // Single block I/O
84/// let data = [0xAB; BLOCK_SIZE];
85/// blk.write_block(0, &data)?;
86/// let readback = blk.read_block(0)?;
87/// assert_eq!(readback, data);
88///
89/// // Multi-block I/O
90/// let multi = vec![0xCD; BLOCK_SIZE * 3];
91/// blk.write_blocks(1, &multi)?;
92/// let multi_read = blk.read_blocks(1, 3)?;
93/// assert_eq!(multi_read, multi);
94/// # Ok::<(), grafos_std::FabricError>(())
95/// ```
96#[derive(Debug)]
97pub struct FabricBlock {
98    num_blocks: u64,
99    lease_state: Option<SharedLeaseState>,
100    host_lease_id: Option<u128>,
101}
102
103impl FabricBlock {
104    /// Perform the FBBU HELLO handshake and return a block handle.
105    ///
106    /// Queries the device for its total block count after the handshake
107    /// succeeds.
108    ///
109    /// # Errors
110    ///
111    /// - [`FabricError::Disconnected`] if the host connection is unavailable.
112    /// - Other [`FabricError`] variants based on the host status code.
113    pub fn hello() -> Result<FabricBlock> {
114        host::fbbu_hello()?;
115        let num_blocks = host::fbbu_get_num_blocks()?;
116        Ok(FabricBlock {
117            num_blocks,
118            lease_state: None,
119            host_lease_id: None,
120        })
121    }
122
123    /// Returns the total number of blocks reported by the device.
124    pub fn num_blocks(&self) -> u64 {
125        self.num_blocks
126    }
127
128    /// Write a single 512-byte block at the given LBA.
129    ///
130    /// # Errors
131    ///
132    /// - [`FabricError::CapacityExceeded`] if `lba` is beyond the device's
133    ///   block count.
134    pub fn write_block(&self, lba: u64, data: &[u8; BLOCK_SIZE]) -> Result<()> {
135        #[cfg(feature = "observe")]
136        let start = std::time::Instant::now();
137        let result = if let Some(lease_id) = self.host_lease_id {
138            if let Some(state) = &self.lease_state {
139                let info = host::fbbu_query(lease_id)?;
140                sync_state_from_host(state, &info);
141                lease::ensure_active(state)?;
142            }
143            host::fbbu_write_block_lease(lease_id, lba, data)
144        } else {
145            if let Some(state) = &self.lease_state {
146                lease::ensure_active(state)?;
147            }
148            host::fbbu_write_block(lba, data)
149        };
150        #[cfg(feature = "observe")]
151        match &result {
152            Ok(()) => {
153                let duration_us = start.elapsed().as_micros() as u64;
154                crate::observe_hooks::on_block_write(BLOCK_SIZE as u64);
155                crate::observe_hooks::on_op_completed(
156                    grafos_observe::OpType::WriteBlock,
157                    duration_us,
158                    BLOCK_SIZE as u64,
159                );
160            }
161            Err(e) => {
162                crate::observe_hooks::on_op_error();
163                crate::observe_hooks::on_op_failed(
164                    grafos_observe::OpType::WriteBlock,
165                    &alloc::format!("{e:?}"),
166                );
167            }
168        }
169        result
170    }
171
172    /// Read a single 512-byte block at the given LBA.
173    ///
174    /// # Errors
175    ///
176    /// - [`FabricError::CapacityExceeded`] if `lba` is beyond the device's
177    ///   block count.
178    pub fn read_block(&self, lba: u64) -> Result<[u8; BLOCK_SIZE]> {
179        #[cfg(feature = "observe")]
180        let start = std::time::Instant::now();
181        let result = if let Some(lease_id) = self.host_lease_id {
182            if let Some(state) = &self.lease_state {
183                let info = host::fbbu_query(lease_id)?;
184                sync_state_from_host(state, &info);
185                lease::ensure_active(state)?;
186            }
187            let mut buf = [0u8; BLOCK_SIZE];
188            host::fbbu_read_block_lease(lease_id, lba, &mut buf)?;
189            Ok(buf)
190        } else {
191            if let Some(state) = &self.lease_state {
192                lease::ensure_active(state)?;
193            }
194            let mut buf = [0u8; BLOCK_SIZE];
195            host::fbbu_read_block(lba, &mut buf)?;
196            Ok(buf)
197        };
198        #[cfg(feature = "observe")]
199        match &result {
200            Ok(_) => {
201                let duration_us = start.elapsed().as_micros() as u64;
202                crate::observe_hooks::on_block_read(BLOCK_SIZE as u64);
203                crate::observe_hooks::on_op_completed(
204                    grafos_observe::OpType::ReadBlock,
205                    duration_us,
206                    BLOCK_SIZE as u64,
207                );
208            }
209            Err(e) => {
210                crate::observe_hooks::on_op_error();
211                crate::observe_hooks::on_op_failed(
212                    grafos_observe::OpType::ReadBlock,
213                    &alloc::format!("{e:?}"),
214                );
215            }
216        }
217        result
218    }
219
220    /// Write multiple contiguous blocks starting at `lba`.
221    ///
222    /// `data.len()` must be a multiple of [`BLOCK_SIZE`] (512). Each
223    /// 512-byte chunk is written as a separate block at consecutive LBAs.
224    ///
225    /// # Errors
226    ///
227    /// - [`FabricError::IoError`] if `data.len()` is not a multiple of
228    ///   [`BLOCK_SIZE`].
229    /// - [`FabricError::CapacityExceeded`] if any LBA is beyond the
230    ///   device's block count.
231    pub fn write_blocks(&self, lba: u64, data: &[u8]) -> Result<()> {
232        if !data.len().is_multiple_of(BLOCK_SIZE) {
233            return Err(FabricError::IoError(-1));
234        }
235        let count = data.len() / BLOCK_SIZE;
236        for i in 0..count {
237            let start = i * BLOCK_SIZE;
238            let block: &[u8; BLOCK_SIZE] = data[start..start + BLOCK_SIZE]
239                .try_into()
240                .map_err(|_| FabricError::IoError(-1))?;
241            self.write_block(lba + i as u64, block)?;
242        }
243        Ok(())
244    }
245
246    /// Read `count` contiguous blocks starting at `lba`.
247    ///
248    /// Returns a `Vec<u8>` of length `count * BLOCK_SIZE`.
249    ///
250    /// # Errors
251    ///
252    /// - [`FabricError::CapacityExceeded`] if any LBA is beyond the
253    ///   device's block count.
254    pub fn read_blocks(&self, lba: u64, count: u32) -> Result<Vec<u8>> {
255        let mut out = vec![0u8; count as usize * BLOCK_SIZE];
256        for i in 0..count {
257            let block = self.read_block(lba + i as u64)?;
258            let start = i as usize * BLOCK_SIZE;
259            out[start..start + BLOCK_SIZE].copy_from_slice(&block);
260        }
261        Ok(out)
262    }
263}
264
265/// A block storage lease that auto-frees on drop.
266///
267/// Wraps a [`FabricBlock`] handle with RAII lifecycle management. When the
268/// `BlockLease` is dropped, the underlying resource will be released (once
269/// the host provides an explicit `fbbu_free()` call).
270///
271/// Created via [`BlockBuilder::acquire`].
272///
273/// # Examples
274///
275/// ```rust
276/// use grafos_std::block::{BlockBuilder, BLOCK_SIZE};
277///
278/// # grafos_std::host::reset_mock();
279/// # grafos_std::host::mock_set_fbbu_num_blocks(128);
280/// let lease = BlockBuilder::new().min_blocks(16).acquire()?;
281/// let data = [42u8; BLOCK_SIZE];
282/// lease.block().write_block(0, &data)?;
283/// // lease is freed when it goes out of scope
284/// # Ok::<(), grafos_std::FabricError>(())
285/// ```
286#[derive(Debug)]
287pub struct BlockLease {
288    state: SharedLeaseState,
289    block: FabricBlock,
290}
291
292impl BlockLease {
293    fn sync_from_host(&self) {
294        let Some(lease_id) = self.block.host_lease_id else {
295            return;
296        };
297        if let Ok(info) = host::fbbu_query(lease_id) {
298            sync_state_from_host(&self.state, &info);
299        }
300    }
301
302    /// Access the underlying [`FabricBlock`] handle for I/O operations.
303    pub fn block(&self) -> &FabricBlock {
304        &self.block
305    }
306
307    /// Lease metadata snapshot (id, creation time, expiry, and status).
308    pub fn info(&self) -> LeaseInfo {
309        self.sync_from_host();
310        lease::info(&self.state)
311    }
312
313    /// Unique lease identifier.
314    pub fn lease_id(&self) -> u128 {
315        lease::lease_id(&self.state)
316    }
317
318    /// Lease creation timestamp (unix seconds).
319    pub fn created_at_unix_secs(&self) -> u64 {
320        lease::created_at_unix_secs(&self.state)
321    }
322
323    /// Lease expiry timestamp (unix seconds).
324    pub fn expires_at_unix_secs(&self) -> u64 {
325        self.sync_from_host();
326        lease::expires_at_unix_secs(&self.state)
327    }
328
329    /// Current lease status.
330    pub fn status(&self) -> LeaseStatus {
331        self.sync_from_host();
332        lease::status(&self.state)
333    }
334
335    /// Renew the lease TTL by `duration_secs`.
336    pub fn renew(&self, duration_secs: u64) -> Result<()> {
337        if let Some(lease_id) = self.block.host_lease_id {
338            let expires_at_unix_secs = host::fbbu_renew(lease_id, duration_secs)?;
339            lease::set_expires_at_unix_secs(&self.state, expires_at_unix_secs);
340            lease::set_status(&self.state, LeaseStatus::Active);
341            return Ok(());
342        }
343        lease::renew(&self.state, duration_secs)
344    }
345
346    /// Explicitly revoke/free this lease.
347    pub fn free(&self) {
348        if let Some(lease_id) = self.block.host_lease_id {
349            let _ = host::fbbu_free(lease_id);
350        }
351        lease::free(&self.state);
352    }
353}
354
355impl Drop for BlockLease {
356    fn drop(&mut self) {
357        #[cfg(feature = "observe")]
358        crate::observe_hooks::on_lease_dropped(LEASE_TAG_BLOCK, lease::lease_id(&self.state));
359        lease::free(&self.state);
360    }
361}
362
363/// Builder for acquiring a fabric block storage lease with capacity constraints.
364///
365/// Use the builder pattern to specify minimum block count requirements,
366/// then call [`acquire`](BlockBuilder::acquire) to perform the HELLO
367/// handshake and validate the device capacity.
368///
369/// # Examples
370///
371/// ```rust
372/// use grafos_std::block::BlockBuilder;
373///
374/// # grafos_std::host::reset_mock();
375/// # grafos_std::host::mock_set_fbbu_num_blocks(1024);
376/// let lease = BlockBuilder::new().min_blocks(256).acquire()?;
377/// assert!(lease.block().num_blocks() >= 256);
378/// # Ok::<(), grafos_std::FabricError>(())
379/// ```
380pub struct BlockBuilder {
381    min_blocks: u64,
382    lease_secs: u64,
383}
384
385impl BlockBuilder {
386    /// Create a new builder with no minimum block count constraint.
387    pub fn new() -> Self {
388        BlockBuilder {
389            min_blocks: 0,
390            lease_secs: 300,
391        }
392    }
393
394    /// Set the minimum number of blocks required from the device.
395    ///
396    /// If the device reports fewer blocks than this value,
397    /// [`acquire`](BlockBuilder::acquire) will return
398    /// [`FabricError::CapacityExceeded`].
399    pub fn min_blocks(mut self, n: u64) -> Self {
400        self.min_blocks = n;
401        self
402    }
403
404    /// Set the lease TTL in seconds.
405    pub fn lease_secs(mut self, secs: u64) -> Self {
406        self.lease_secs = secs.max(1);
407        self
408    }
409
410    /// Acquire the block lease by performing the HELLO handshake and
411    /// verifying that the device meets the requested minimum.
412    ///
413    /// # Errors
414    ///
415    /// - [`FabricError::CapacityExceeded`] if the device block count is
416    ///   less than the configured [`min_blocks`](BlockBuilder::min_blocks).
417    /// - [`FabricError::Disconnected`] if the HELLO handshake fails.
418    pub fn acquire(self) -> Result<BlockLease> {
419        let result = match host::fbbu_alloc(self.min_blocks, self.lease_secs) {
420            Ok(info) => {
421                if info.num_blocks < self.min_blocks {
422                    return Err(FabricError::CapacityExceeded);
423                }
424                let created_at_unix_secs = info
425                    .expires_at_unix_secs
426                    .saturating_sub(self.lease_secs.max(1));
427                let state = lease::new_shared_lease_from_parts(
428                    info.lease_id,
429                    created_at_unix_secs,
430                    info.expires_at_unix_secs,
431                    lease_status_from_host(info.status),
432                );
433                Ok(BlockLease {
434                    state: state.clone(),
435                    block: FabricBlock {
436                        num_blocks: info.num_blocks,
437                        lease_state: Some(state),
438                        host_lease_id: Some(info.lease_id),
439                    },
440                })
441            }
442            Err(FabricError::Unsupported) => {
443                let state = lease::new_shared_lease(LEASE_TAG_BLOCK, self.lease_secs);
444                let mut block = FabricBlock::hello()?;
445                block.lease_state = Some(state.clone());
446                if block.num_blocks() < self.min_blocks {
447                    return Err(FabricError::CapacityExceeded);
448                }
449                Ok(BlockLease { state, block })
450            }
451            Err(e) => Err(e),
452        };
453        #[cfg(feature = "observe")]
454        if let Ok(ref lease) = result {
455            crate::observe_hooks::on_lease_acquired(
456                LEASE_TAG_BLOCK,
457                lease.lease_id(),
458                "local",
459                lease.block().num_blocks() * BLOCK_SIZE as u64,
460            );
461        }
462        result
463    }
464
465    /// Attach to an existing block lease by ID.
466    ///
467    /// Instead of allocating a new lease, this queries the host for the
468    /// given `lease_id` and returns a [`BlockLease`] bound to it if the
469    /// lease is still active. This enables crash recovery: a replacement
470    /// tasklet can reconnect to block storage that survived the previous
471    /// tasklet's death.
472    ///
473    /// # Errors
474    ///
475    /// - [`FabricError::LeaseExpired`] if the lease has expired.
476    /// - [`FabricError::Revoked`] if the lease has been revoked.
477    /// - [`FabricError::Disconnected`] if the query fails.
478    pub fn attach(lease_id: u128) -> Result<BlockLease> {
479        let info = host::fbbu_query(lease_id)?;
480        let status = lease_status_from_host(info.status);
481        match status {
482            LeaseStatus::Active => {}
483            LeaseStatus::Expired => return Err(FabricError::LeaseExpired),
484            LeaseStatus::Revoked => return Err(FabricError::Revoked),
485        }
486        let created_at_unix_secs = 0;
487        let state = lease::new_shared_lease_from_parts(
488            info.lease_id,
489            created_at_unix_secs,
490            info.expires_at_unix_secs,
491            status,
492        );
493        let result = BlockLease {
494            state: state.clone(),
495            block: FabricBlock {
496                num_blocks: info.num_blocks,
497                lease_state: Some(state),
498                host_lease_id: Some(info.lease_id),
499            },
500        };
501        #[cfg(feature = "observe")]
502        crate::observe_hooks::on_lease_acquired(
503            LEASE_TAG_BLOCK,
504            result.lease_id(),
505            "attach",
506            result.block().num_blocks() * BLOCK_SIZE as u64,
507        );
508        Ok(result)
509    }
510}
511
512impl Default for BlockBuilder {
513    fn default() -> Self {
514        Self::new()
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use crate::host;
522
523    #[test]
524    fn block_hello_write_read_roundtrip() {
525        host::reset_mock();
526        host::mock_set_fbbu_num_blocks(128);
527
528        let blk = FabricBlock::hello().expect("hello");
529        assert_eq!(blk.num_blocks(), 128);
530
531        let mut data = [0u8; BLOCK_SIZE];
532        for (i, byte) in data.iter_mut().enumerate() {
533            *byte = (i & 0xFF) as u8;
534        }
535        blk.write_block(0, &data).expect("write");
536
537        let readback = blk.read_block(0).expect("read");
538        assert_eq!(readback, data);
539    }
540
541    #[test]
542    fn block_hello_error_propagates() {
543        host::reset_mock();
544        host::mock_set_fbbu_hello_error(Some(-1));
545
546        let result = FabricBlock::hello();
547        assert!(result.is_err());
548        assert_eq!(result.unwrap_err(), FabricError::Disconnected);
549
550        host::mock_set_fbbu_hello_error(None);
551    }
552
553    #[test]
554    fn block_multi_block_write_read() {
555        host::reset_mock();
556        host::mock_set_fbbu_num_blocks(64);
557
558        let blk = FabricBlock::hello().expect("hello");
559
560        let mut data = vec![0u8; BLOCK_SIZE * 3];
561        for (i, b) in data.iter_mut().enumerate() {
562            *b = (i.wrapping_mul(7) & 0xFF) as u8;
563        }
564        blk.write_blocks(0, &data).expect("write_blocks");
565
566        let readback = blk.read_blocks(0, 3).expect("read_blocks");
567        assert_eq!(readback, data);
568    }
569
570    #[test]
571    fn block_builder_checks_capacity() {
572        host::reset_mock();
573        host::mock_set_fbbu_num_blocks(16);
574
575        let result = BlockBuilder::new().min_blocks(32).acquire();
576        assert_eq!(result.unwrap_err(), FabricError::CapacityExceeded);
577
578        let lease = BlockBuilder::new()
579            .min_blocks(16)
580            .acquire()
581            .expect("acquire");
582        assert_eq!(lease.block().num_blocks(), 16);
583    }
584
585    #[test]
586    fn block_write_blocks_rejects_non_aligned() {
587        host::reset_mock();
588        host::mock_set_fbbu_num_blocks(64);
589
590        let blk = FabricBlock::hello().expect("hello");
591        let bad = vec![0u8; 100]; // not a multiple of 512
592        assert!(blk.write_blocks(0, &bad).is_err());
593    }
594
595    #[test]
596    fn block_lease_expires_and_rejects_io() {
597        host::reset_mock();
598        host::mock_set_fbbu_num_blocks(16);
599        host::mock_set_unix_time_secs(2_000);
600
601        let lease = BlockBuilder::new()
602            .lease_secs(5)
603            .acquire()
604            .expect("acquire");
605        assert_eq!(lease.status(), LeaseStatus::Active);
606        host::mock_advance_time_secs(6);
607        assert_eq!(lease.status(), LeaseStatus::Expired);
608
609        let data = [0u8; BLOCK_SIZE];
610        assert_eq!(
611            lease.block().write_block(0, &data).unwrap_err(),
612            FabricError::LeaseExpired
613        );
614    }
615
616    #[test]
617    fn block_attach_reconnects_to_active_lease() {
618        host::reset_mock();
619        host::mock_set_fbbu_num_blocks(64);
620
621        let lease = BlockBuilder::new()
622            .lease_secs(300)
623            .acquire()
624            .expect("acquire");
625        let id = lease.lease_id();
626
627        // Write data through the original lease.
628        let mut data = [0u8; BLOCK_SIZE];
629        data[0] = 0x42;
630        lease.block().write_block(0, &data).expect("write");
631
632        // Attach to the same lease by ID.
633        let attached = BlockBuilder::attach(id).expect("attach");
634        assert_eq!(attached.lease_id(), id);
635        assert_eq!(attached.status(), LeaseStatus::Active);
636
637        // Read back data through the attached lease.
638        let readback = attached.block().read_block(0).expect("read");
639        assert_eq!(readback[0], 0x42);
640    }
641
642    #[test]
643    fn block_attach_expired_returns_error() {
644        host::reset_mock();
645        host::mock_set_fbbu_num_blocks(64);
646        host::mock_set_unix_time_secs(1_000);
647
648        let lease = BlockBuilder::new()
649            .lease_secs(5)
650            .acquire()
651            .expect("acquire");
652        let id = lease.lease_id();
653
654        // Advance past expiry.
655        host::mock_advance_time_secs(10);
656
657        let result = BlockBuilder::attach(id);
658        assert_eq!(result.unwrap_err(), FabricError::LeaseExpired);
659    }
660
661    #[test]
662    fn block_attach_revoked_returns_error() {
663        host::reset_mock();
664        host::mock_set_fbbu_num_blocks(64);
665
666        let lease = BlockBuilder::new().acquire().expect("acquire");
667        let id = lease.lease_id();
668        lease.free(); // revoke
669
670        let result = BlockBuilder::attach(id);
671        assert_eq!(result.unwrap_err(), FabricError::Revoked);
672    }
673
674    #[test]
675    fn block_attach_unknown_lease_returns_error() {
676        host::reset_mock();
677        host::mock_set_fbbu_num_blocks(64);
678
679        let result = BlockBuilder::attach(0xDEADBEEF);
680        assert!(result.is_err());
681    }
682
683    #[test]
684    fn block_leases_are_isolated_by_lease_id() {
685        host::reset_mock();
686        host::mock_set_fbbu_num_blocks(8);
687
688        let lease_a = BlockBuilder::new().acquire().expect("acquire a");
689        let lease_b = BlockBuilder::new().acquire().expect("acquire b");
690
691        let mut a = [0u8; BLOCK_SIZE];
692        let mut b = [0u8; BLOCK_SIZE];
693        a[0] = 0xAA;
694        b[0] = 0xBB;
695
696        lease_a.block().write_block(0, &a).expect("write a");
697        lease_b.block().write_block(0, &b).expect("write b");
698
699        let read_a = lease_a.block().read_block(0).expect("read a");
700        let read_b = lease_b.block().read_block(0).expect("read b");
701        assert_eq!(read_a[0], 0xAA);
702        assert_eq!(read_b[0], 0xBB);
703    }
704}