use std::time::Duration;
use futures::stream::{self, FuturesOrdered, Stream, StreamExt};
use tokio::io::{AsyncRead, AsyncReadExt};
use tokio::sync::{mpsc, oneshot, Mutex};
use tokio::time::sleep;
use tracing::debug;
use rpc::{CommitmentLevel, Rpc};
use tape_api::program::tapedrive::track_pda;
use tape_api::state::Tape;
use tape_core::prelude::CompressedTrack;
use tape_core::track::data::{track_key, BlobDataSlice};
use tape_core::track::mirror::ArchiveMirror;
use tape_core::track::types::CompressedTrackProof;
use tape_core::types::ContentType;
use tape_core::types::{StorageUnits, TrackNumber};
use tape_crypto::hash::hash;
use tape_crypto::Hash;
use tape_protocol::Api;
use tape_protocol::api::CertifyRes;
use tape_retry::{retry_if, RetryConfig};
use crate::error::TapedriveError;
use crate::keys::operator::TapeOperator;
use crate::keys::tape_key::TapeKey;
use crate::metrics::{Operation, Phase};
use crate::tapedrive::Tapedrive;
use crate::track::write::{
certified_track, certify_submit_with_retry, certify_with_retry, coded_identity,
collect_certification, encode_blob, ensure_track_matches, finish_coded_track, inline_write_fits,
register_blob_processed, resolve_sent_blob, should_retry_certification,
submit_blob_with_logical_size, submit_certification_with_proof, submit_raw_with_logical_size,
upload_with_retry, wait_for_certified_track, UploadPlan, WrittenTrack, UNNAMED_TRACK,
UNTYPED_TRACK,
};
use crate::transfer::certify::CollectedSignatures;
use super::error::StreamError;
use super::manifest::{
ChunkEntry, ChunkManifest, MAX_TRACK_SIZE, MAX_TRACKS_PER_TAPE, MANIFEST_VERSION,
};
use super::receipt::StreamReceipt;
const MAX_TRACKS: TrackNumber = TrackNumber(MAX_TRACKS_PER_TAPE);
const ENCODE_AHEAD: usize = 2;
const MAX_ENCODE_WORKERS: usize = 4;
const RESOLVE_CONCURRENCY: usize = 3;
const COLLECT_LOOKAHEAD: usize = 2;
const CERTIFY_CONFLICT_ATTEMPTS: usize = 3;
const CERTIFY_CONFLICT_DELAY: Duration = Duration::from_millis(400);
impl<Blockchain: Rpc, Cluster: Api> Tapedrive<Blockchain, Cluster> {
pub async fn store_named_stream<Reader: AsyncRead + Unpin>(
&self,
tape_key: &TapeKey,
name: impl AsRef<[u8]>,
content_type: ContentType,
size: StorageUnits,
reader: Reader,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
self.store_named_stream_as(tape_key, name, content_type, size, reader)
.await
}
pub async fn store_named_stream_as<Reader: AsyncRead + Unpin>(
&self,
operator: &impl TapeOperator,
name: impl AsRef<[u8]>,
content_type: ContentType,
size: StorageUnits,
reader: Reader,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
let timer = self
.timer(Operation::WriteStream, Phase::Total)
.bytes(size.to_bytes());
let result = store_stream(
self,
operator,
name.as_ref(),
content_type,
size,
reader,
)
.await;
timer.finish_result(&result);
result
}
}
pub async fn write_bytes<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
data: &[u8],
) -> Result<StreamReceipt, TapedriveError> {
let (manifest, receipts) =
store_bytes(client, tape_key, name, content_type, data).await?;
complete_stream(client, tape_key, &manifest, &receipts).await
}
pub async fn write_stream<Blockchain: Rpc, Cluster: Api, Reader: AsyncRead + Unpin>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
size: StorageUnits,
reader: Reader,
) -> Result<StreamReceipt, TapedriveError> {
let (manifest, receipts) =
store_stream(client, tape_key, name, content_type, size, reader).await?;
complete_stream(client, tape_key, &manifest, &receipts).await
}
struct PendingChunk {
pub entry: ChunkEntry,
pub written: WrittenTrack,
pub receipts: Vec<CertifyRes>,
}
struct RegisteredChunk {
entry: ChunkEntry,
written: WrittenTrack,
plan: UploadPlan,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ManifestWriteMode {
Inline,
Coded,
}
fn validate_stream_size(size: StorageUnits) -> Result<(), StreamError> {
if size.is_zero() {
return Err(StreamError::InvalidInput(
"empty streams are not supported".into(),
));
}
Ok(())
}
fn chunk_count_for_size(size: StorageUnits) -> Result<TrackNumber, StreamError> {
let chunk_count = size.to_bytes().div_ceil(MAX_TRACK_SIZE as u64);
Ok(TrackNumber(chunk_count))
}
fn chunk_offset(chunk_index: usize) -> Result<StorageUnits, StreamError> {
let chunk_index = u64::try_from(chunk_index)
.map_err(|_| StreamError::InvalidInput("stream has too many chunks".into()))?;
let offset = chunk_index
.checked_mul(MAX_TRACK_SIZE as u64)
.ok_or_else(|| StreamError::InvalidInput("stream size overflow".into()))?;
Ok(StorageUnits::from_bytes(offset))
}
fn chunk_size(
chunk_index: usize,
chunk_count: TrackNumber,
total_size: StorageUnits,
) -> Result<StorageUnits, StreamError> {
if chunk_index + 1 == chunk_count.as_usize() {
let offset = chunk_offset(chunk_index)?;
total_size
.checked_sub(offset)
.ok_or_else(|| StreamError::InvalidInput("stream size underflow".into()))
} else {
Ok(StorageUnits::from_bytes(MAX_TRACK_SIZE as u64))
}
}
fn build_entries(
start_track_number: TrackNumber,
chunk_count: TrackNumber,
total_size: StorageUnits,
) -> Result<Vec<ChunkEntry>, StreamError> {
let mut entries = Vec::with_capacity(chunk_count.as_usize());
for chunk_index in 0..chunk_count.as_usize() {
let track_number = start_track_number
.checked_add(TrackNumber(chunk_index as u64))
.ok_or_else(|| StreamError::InvalidInput("chunk track number overflow".into()))?;
entries.push(ChunkEntry {
track_number,
offset: chunk_offset(chunk_index)?,
size: chunk_size(chunk_index, chunk_count, total_size)?,
});
}
Ok(entries)
}
fn build_manifest(
key: Hash,
total_size: StorageUnits,
entries: Vec<ChunkEntry>,
) -> Result<ChunkManifest, StreamError> {
let chunk_count = TrackNumber(
u64::try_from(entries.len())
.map_err(|_| StreamError::InvalidInput("stream has too many chunks".into()))?,
);
Ok(ChunkManifest {
version: MANIFEST_VERSION,
total_size,
chunk_count,
chunk_size: StorageUnits::from_bytes(MAX_TRACK_SIZE as u64),
key,
chunks: entries,
})
}
fn preflight(
tape: &Tape,
total_required_bytes: StorageUnits,
tracks_needed: TrackNumber,
) -> Result<(), TapedriveError> {
let available_capacity = tape.capacity.saturating_sub(tape.used);
if total_required_bytes > available_capacity {
return Err(TapedriveError::InsufficientCapacity {
need: total_required_bytes,
available: available_capacity,
});
}
let used_tracks = tape.tracks.next_number();
let available_tracks = MAX_TRACKS.saturating_sub(used_tracks);
if tracks_needed > available_tracks {
let chunk_count = tracks_needed
.checked_prev()
.ok_or_else(|| stream_error(StreamError::InvalidInput("stream needs no data tracks".into())))?;
return Err(stream_error(StreamError::InsufficientTrackSlots {
available: available_tracks,
needed: tracks_needed,
chunks: chunk_count,
}));
}
Ok(())
}
async fn store_bytes<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
data: &[u8],
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
let size = StorageUnits::from_bytes(data.len() as u64);
let chunk_count = chunk_count_for_size(size).map_err(stream_error)?;
let first = stream_chunk(data, 0, chunk_count, size)?;
if is_stream_resume(client, tape_key, first).await? {
return resume_stream(client, tape_key, name, content_type, data, size, chunk_count).await;
}
let (tape, chunk_count) = prepare_write(client, tape_key, name, size).await?;
let chunk_sources = stream::iter(
data.chunks(MAX_TRACK_SIZE)
.map(|chunk| Ok::<_, TapedriveError>(chunk.to_vec())),
);
let pending_chunks =
pipeline_chunks(client, tape_key, &tape, size, chunk_count, chunk_sources).await?;
store_manifest_for_chunks(client, tape_key, name, content_type, size, pending_chunks).await
}
async fn store_stream<Blockchain: Rpc, Cluster: Api, Reader: AsyncRead + Unpin>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
size: StorageUnits,
mut reader: Reader,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
validate_stream_size(size).map_err(stream_error)?;
let chunk_count = chunk_count_for_size(size).map_err(stream_error)?;
let first = read_chunk(&mut reader, 0, chunk_count, size).await?;
if is_stream_resume(client, tape_key, &first).await? {
return resume_stream_reader(
client, tape_key, name, content_type, size, chunk_count, first, reader,
)
.await;
}
let (tape, chunk_count) = prepare_write(client, tape_key, name, size).await?;
let chunk_sources = stream::once(async move { Ok::<_, TapedriveError>(first) }).chain(
stream::unfold((&mut reader, 1usize), move |(reader, chunk_index)| async move {
if chunk_index >= chunk_count.as_usize() {
return None;
}
let result = read_chunk(reader, chunk_index, chunk_count, size).await;
Some((result, (reader, chunk_index + 1)))
}),
);
let pending_chunks =
pipeline_chunks(client, tape_key, &tape, size, chunk_count, chunk_sources).await?;
verify_stream_drained(&mut reader).await?;
store_manifest_for_chunks(client, tape_key, name, content_type, size, pending_chunks).await
}
async fn resume_stream_reader<Blockchain: Rpc, Cluster: Api, Reader: AsyncRead + Unpin>(
client: &Tapedrive<Blockchain, Cluster>,
operator: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
size: StorageUnits,
chunk_count: TrackNumber,
first: Vec<u8>,
mut reader: Reader,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
let timer = client
.timer(Operation::WriteStream, Phase::Total)
.bytes(size.to_bytes());
let result = async {
resume_stream_chunk(client, operator, &first, TrackNumber(0)).await?;
for chunk_index in 1..chunk_count.as_usize() {
let chunk = read_chunk(&mut reader, chunk_index, chunk_count, size).await?;
resume_stream_chunk(client, operator, &chunk, TrackNumber(chunk_index as u64)).await?;
}
verify_stream_drained(&mut reader).await?;
store_resumed_manifest(client, operator, name, content_type, size, chunk_count).await
}
.await;
timer.finish_result(&result);
result
}
async fn prepare_write<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
size: StorageUnits,
) -> Result<(Tape, TrackNumber), TapedriveError> {
let timer = client
.timer(Operation::WriteStream, Phase::Preflight)
.bytes(size.to_bytes());
let result = async {
validate_stream_size(size).map_err(stream_error)?;
let chunk_count = chunk_count_for_size(size).map_err(stream_error)?;
let tracks_needed = chunk_count.checked_next().ok_or_else(|| {
stream_error(StreamError::InvalidInput("stream has too many chunks".into()))
})?;
let entries = build_entries(TrackNumber(0), chunk_count, size).map_err(stream_error)?;
let manifest = build_manifest(hash(name), size, entries).map_err(stream_error)?;
let manifest_bytes = manifest.to_bytes().map_err(stream_error)?;
let total_size = size
.checked_add(StorageUnits::from_bytes(manifest_bytes.len() as u64))
.ok_or_else(|| stream_error(StreamError::InvalidInput("stream size overflow".into())))?;
let tape = client.get_tape(&tape_key.address()).await?;
preflight(&tape, total_size, tracks_needed)?;
Ok((tape, chunk_count))
}
.await;
timer.finish_result(&result);
result
}
async fn is_stream_resume<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
operator: &impl TapeOperator,
first_chunk: &[u8],
) -> Result<bool, TapedriveError> {
let tape = client.get_tape(&operator.address()).await?;
if tape.tracks.next_number() == TrackNumber(0) {
return Ok(false);
}
stream_chunk_matches(client, operator, TrackNumber(0), first_chunk).await
}
fn stream_chunk(
data: &[u8],
chunk_index: usize,
chunk_count: TrackNumber,
size: StorageUnits,
) -> Result<&[u8], TapedriveError> {
let start = chunk_offset(chunk_index).map_err(stream_error)?.as_usize();
let len = chunk_size(chunk_index, chunk_count, size)
.map_err(stream_error)?
.as_usize();
data.get(start..start + len)
.ok_or_else(|| stream_error(StreamError::InvalidInput("chunk out of range".into())))
}
async fn stream_chunk_matches<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
track_number: TrackNumber,
chunk_data: &[u8],
) -> Result<bool, TapedriveError> {
let existing = match client
.get_track_by_number(&tape_key.address(), track_number)
.await
{
Ok(track) => track,
Err(TapedriveError::NotFound) => return Ok(false),
Err(other) => return Err(other),
};
if !existing.is_coded() || existing.size.to_bytes() != chunk_data.len() as u64 {
return Ok(false);
}
let (_, key, value_hash) = coded_identity(client, UNNAMED_TRACK, chunk_data, Operation::WriteStream).await?;
Ok(existing.key == key && existing.value_hash == value_hash)
}
async fn resume_stream<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
data: &[u8],
size: StorageUnits,
chunk_count: TrackNumber,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
let timer = client
.timer(Operation::WriteStream, Phase::Total)
.bytes(size.to_bytes());
let result = async {
for chunk_index in 0..chunk_count.as_usize() {
let chunk = stream_chunk(data, chunk_index, chunk_count, size)?;
resume_stream_chunk(client, tape_key, chunk, TrackNumber(chunk_index as u64)).await?;
}
store_resumed_manifest(client, tape_key, name, content_type, size, chunk_count).await
}
.await;
timer.finish_result(&result);
result
}
async fn store_resumed_manifest<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
operator: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
size: StorageUnits,
chunk_count: TrackNumber,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
let entries = build_entries(TrackNumber(0), chunk_count, size).map_err(stream_error)?;
let manifest = build_manifest(hash(name), size, entries).map_err(stream_error)?;
let manifest_bytes = manifest.to_bytes().map_err(stream_error)?;
ensure_stream_manifest(
client,
operator,
name,
content_type,
size,
&manifest_bytes,
chunk_count,
)
.await
}
async fn resume_stream_chunk<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
chunk_data: &[u8],
track_number: TrackNumber,
) -> Result<CompressedTrack, TapedriveError> {
let tape = tape_key.address();
match client.get_track_by_number(&tape, track_number).await {
Ok(existing) => {
let (plan, key, value_hash) =
coded_identity(client, UNNAMED_TRACK, chunk_data, Operation::WriteStream).await?;
ensure_track_matches(&existing, key, value_hash)?;
finish_coded_track(client, tape_key, existing, &plan, Operation::WriteStream).await
}
Err(TapedriveError::NotFound) => {
let logical_size = StorageUnits::from_bytes(chunk_data.len() as u64);
let (written, plan) = submit_blob_with_logical_size(
client,
tape_key,
UNNAMED_TRACK,
UNTYPED_TRACK,
logical_size,
chunk_data,
Operation::WriteStream,
)
.await?;
verify_track_number(&written, track_number)?;
let receipts =
upload_with_retry(client, &written, &plan, Operation::WriteStream).await?;
certify_with_retry(client, tape_key, &written, Operation::WriteStream, &receipts).await
}
Err(other) => Err(other),
}
}
async fn ensure_stream_manifest<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
size: StorageUnits,
manifest_bytes: &[u8],
track_number: TrackNumber,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
let tape = tape_key.address();
let existing = match client.get_track_by_number(&tape, track_number).await {
Ok(track) => track,
Err(TapedriveError::NotFound) => {
let (written, receipts) =
store_manifest(client, tape_key, name, content_type, size, manifest_bytes).await?;
verify_track_number(&written, track_number)?;
return Ok((written, receipts));
}
Err(other) => return Err(other),
};
match manifest_write_mode(name, manifest_bytes) {
ManifestWriteMode::Inline => {
let slice = BlobDataSlice::Inline(manifest_bytes);
let meta = slice.meta().ok_or_else(|| {
TapedriveError::Encoding("inline manifest has no commitment".into())
})?;
ensure_track_matches(&existing, track_key(name, &slice), meta.value_hash)?;
Ok((
WrittenTrack {
address: track_pda(tape, track_number).0,
track: existing,
},
Vec::new(),
))
}
ManifestWriteMode::Coded => {
let (plan, key, value_hash) =
coded_identity(client, name, manifest_bytes, Operation::WriteStream).await?;
ensure_track_matches(&existing, key, value_hash)?;
let written = WrittenTrack {
address: track_pda(tape, track_number).0,
track: existing,
};
if written.track.is_certified() {
return Ok((written, Vec::new()));
}
let receipts =
upload_with_retry(client, &written, &plan, Operation::WriteStream).await?;
Ok((written, receipts))
}
}
}
fn verify_track_number(
written: &WrittenTrack,
expected: TrackNumber,
) -> Result<(), TapedriveError> {
if written.track.track_number == expected {
Ok(())
} else {
Err(stream_error(StreamError::Integrity(format!(
"resume expected track {expected}, registered at {}",
written.track.track_number
))))
}
}
async fn pipeline_chunks<Blockchain, Cluster, Chunks>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
tape: &Tape,
size: StorageUnits,
chunk_count: TrackNumber,
chunk_sources: Chunks,
) -> Result<Vec<PendingChunk>, TapedriveError>
where
Blockchain: Rpc,
Cluster: Api,
Chunks: Stream<Item = Result<Vec<u8>, TapedriveError>>,
{
let mirror = Mutex::new(ArchiveMirror::new(&tape.tracks));
let mirror = &mirror;
let (encoded_sender, mut encoded_receiver) = mpsc::channel(ENCODE_AHEAD);
let (sent_sender, mut sent_receiver) = mpsc::channel(1);
let (registered_sender, mut registered_receiver) = mpsc::channel(1);
let (stored_sender, mut stored_receiver) = mpsc::channel(chunk_count.as_usize().max(1));
let (registers_mirrored_sender, registers_mirrored_receiver) = oneshot::channel::<()>();
let encode_stage = async move {
let workers = std::thread::available_parallelism()
.map(|cores| cores.get())
.unwrap_or(1)
.min(MAX_ENCODE_WORKERS);
let mut in_flight = FuturesOrdered::new();
let mut chunk_sources = std::pin::pin!(chunk_sources);
let mut chunk_index = 0usize;
while let Some(chunk_data) = chunk_sources.next().await {
if in_flight.len() >= workers {
let Some(encoded) = in_flight.next().await else { break };
if encoded_sender.send(encoded?).await.is_err() {
return Ok(());
}
}
let data = chunk_data?;
let index = chunk_index;
in_flight.push_back(async move {
encode_blob(client, data, Operation::WriteStream)
.await
.map(|plan| (index, plan))
});
chunk_index += 1;
}
while let Some(encoded) = in_flight.next().await {
if encoded_sender.send(encoded?).await.is_err() {
break;
}
}
Ok::<_, TapedriveError>(())
};
let register_stage = async move {
while let Some((chunk_index, plan)) = encoded_receiver.recv().await {
let logical_size = plan.storage_units;
let sent = register_blob_processed(
client,
tape_key,
UNNAMED_TRACK,
UNTYPED_TRACK,
logical_size,
plan,
Operation::WriteStream,
)
.await?;
if sent_sender.send((chunk_index, sent)).await.is_err() {
break;
}
}
Ok::<_, TapedriveError>(())
};
let resolve_stage = async move {
let mut in_flight = FuturesOrdered::new();
let mut is_registering = true;
while is_registering || !in_flight.is_empty() {
tokio::select! {
sent = sent_receiver.recv(),
if is_registering && in_flight.len() < RESOLVE_CONCURRENCY =>
{
match sent {
Some((chunk_index, sent)) => in_flight.push_back(async move {
resolve_sent_blob(client, sent)
.await
.map(|resolved| (chunk_index, resolved))
}),
None => is_registering = false,
}
}
resolved = in_flight.next(), if !in_flight.is_empty() => {
let Some(resolved) = resolved else { continue };
let (chunk_index, (written, plan)) = resolved?;
append_to_mirror(mirror, &written).await?;
let registered = RegisteredChunk {
entry: ChunkEntry {
track_number: written.track.track_number,
offset: chunk_offset(chunk_index).map_err(stream_error)?,
size: chunk_size(chunk_index, chunk_count, size)
.map_err(stream_error)?,
},
written,
plan,
};
if registered_sender.send(registered).await.is_err() {
break;
}
}
}
}
let _ = registers_mirrored_sender.send(());
Ok::<_, TapedriveError>(())
};
let store_stage = async move {
let store_depth = client.write_options.store_depth.max(1);
let mut in_flight = FuturesOrdered::new();
let mut is_registering = true;
while is_registering || !in_flight.is_empty() {
tokio::select! {
registered = registered_receiver.recv(),
if is_registering && in_flight.len() < store_depth =>
{
match registered {
Some(registered) => in_flight.push_back(store_chunk(client, registered)),
None => is_registering = false,
}
}
stored = in_flight.next(), if !in_flight.is_empty() => {
let Some(stored) = stored else { continue };
if stored_sender.send(stored?).await.is_err() {
break;
}
}
}
}
Ok::<_, TapedriveError>(())
};
let (collected_sender, mut collected_receiver) = mpsc::channel(COLLECT_LOOKAHEAD);
let collect_stage = async move {
while let Some(pending) = stored_receiver.recv().await {
let collected =
collect_certification(
client,
&pending.written,
Operation::WriteStream,
&pending.receipts,
)
.await?;
if collected_sender.send((pending, collected)).await.is_err() {
break;
}
}
Ok::<_, TapedriveError>(())
};
let certify_stage = async move {
registers_mirrored_receiver.await.map_err(|_| {
stream_error(StreamError::Chunk(
"resolve stage ended before mirroring every register".into(),
))
})?;
let mut pending_chunks = Vec::with_capacity(chunk_count.as_usize());
while let Some((pending, collected)) = collected_receiver.recv().await {
certify_chunk(
client,
tape_key,
mirror,
&pending.written,
collected,
&pending.receipts,
Operation::WriteStream,
)
.await?;
pending_chunks.push(pending);
}
let visible = client
.timer(Operation::WriteStream, Phase::CertifyVisible)
.chunks(pending_chunks.len() as u64);
let tape_address = tape_key.address();
let result = futures::future::try_join_all(pending_chunks.iter().map(|pending| {
wait_for_certified_track(client, &tape_address, pending.written.track.track_number)
}))
.await;
visible.finish_result(&result);
result?;
verify_mirror_root(client, tape_key, mirror).await?;
Ok::<_, TapedriveError>(pending_chunks)
};
let ((), (), (), (), (), pending_chunks) = tokio::try_join!(
encode_stage,
register_stage,
resolve_stage,
store_stage,
collect_stage,
certify_stage
)?;
Ok(pending_chunks)
}
pub(crate) async fn append_to_mirror(
mirror: &Mutex<ArchiveMirror>,
written: &WrittenTrack,
) -> Result<(), TapedriveError> {
let mut mirror = mirror.lock().await;
if written.track.track_number < mirror.next_number() {
return Ok(());
}
mirror.append(&written.track).map_err(|_| {
stream_error(StreamError::Integrity(format!(
"track {} arrived out of mirror order; an external writer touched the tape",
written.track.track_number
)))
})
}
pub(crate) async fn certify_chunk<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
mirror: &Mutex<ArchiveMirror>,
written: &WrittenTrack,
collected: CollectedSignatures,
banked: &[CertifyRes],
operation: Operation,
) -> Result<(), TapedriveError> {
let track_number = written.track.track_number;
let certified = certified_track(&written.track);
let proof = mirror.lock().await.proof_for(track_number);
if let Ok(proof) = proof {
let submitted = submit_certification_with_proof(
client,
tape_key,
proof,
&collected,
CommitmentLevel::Processed,
operation,
)
.await;
match submitted {
Ok(()) => {
return apply_certified_to_mirror(client, tape_key, mirror, &certified, Some(&proof))
.await
}
Err(err) if should_retry_certification(&err) => {
let done =
recertify_after_conflict(
client,
tape_key,
mirror,
&certified,
&collected,
operation,
)
.await?;
if done {
return Ok(());
}
}
Err(err) => return Err(err),
}
}
certify_submit_with_retry(client, tape_key, written, operation, Some(collected), banked)
.await?;
apply_certified_to_mirror(client, tape_key, mirror, &certified, None).await
}
async fn recertify_after_conflict<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
mirror: &Mutex<ArchiveMirror>,
certified: &CompressedTrack,
collected: &CollectedSignatures,
operation: Operation,
) -> Result<bool, TapedriveError> {
for _ in 0..CERTIFY_CONFLICT_ATTEMPTS {
sleep(CERTIFY_CONFLICT_DELAY).await;
let reseeded = reseed_mirror(client, tape_key).await?;
let proof = {
let mut mirror = mirror.lock().await;
if reseeded.next_number() != mirror.next_number()
|| reseeded.root() != mirror.root()
{
*mirror = reseeded;
return Ok(false);
}
mirror.proof_for(certified.track_number)
};
let Ok(proof) = proof else {
return Ok(false);
};
let submitted = submit_certification_with_proof(
client,
tape_key,
proof,
collected,
CommitmentLevel::Processed,
operation,
)
.await;
match submitted {
Ok(()) => {
apply_certified_to_mirror(client, tape_key, mirror, certified, Some(&proof))
.await?;
return Ok(true);
}
Err(err) if should_retry_certification(&err) => {}
Err(err) => return Err(err),
}
}
Ok(false)
}
async fn reseed_mirror<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
) -> Result<ArchiveMirror, TapedriveError> {
let tape = client.get_tape(&tape_key.address()).await?;
Ok(ArchiveMirror::new(&tape.tracks))
}
async fn apply_certified_to_mirror<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
mirror: &Mutex<ArchiveMirror>,
certified: &CompressedTrack,
proof: Option<&CompressedTrackProof>,
) -> Result<(), TapedriveError> {
let applied = {
let mut mirror = mirror.lock().await;
match proof {
Some(proof) => {
mirror.apply_certified_with_proof(certified.track_number, certified, proof)
}
None => mirror.apply_certified(certified.track_number, certified),
}
};
if applied.is_ok() {
return Ok(());
}
let reseeded = reseed_mirror(client, tape_key).await?;
*mirror.lock().await = reseeded;
Ok(())
}
fn root_poll_config() -> RetryConfig {
RetryConfig {
base_delay: Duration::from_millis(400),
max_delay: Duration::from_millis(400),
max_retries: Some(10),
}
}
pub(crate) async fn verify_mirror_root<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
mirror: &Mutex<ArchiveMirror>,
) -> Result<(), TapedriveError> {
if mirror.lock().await.is_empty() {
debug!(tape = %tape_key.address(), "mirror holds no appended tracks; skipping root check");
return Ok(());
}
retry_if(
root_poll_config(),
None,
|| async {
let expected = mirror.lock().await.root();
let tape = client.get_tape(&tape_key.address()).await?;
let observed = tape.tracks.tree.root();
if observed == expected {
return Ok(());
}
Err(stream_error(StreamError::Integrity(format!(
"tape track tree diverged from the stream mirror: mirror root {expected}, chain root {observed}"
))))
},
|err| matches!(err, TapedriveError::Stream(_)),
)
.await
}
async fn read_chunk<Reader: AsyncRead + Unpin>(
reader: &mut Reader,
chunk_index: usize,
chunk_count: TrackNumber,
total_size: StorageUnits,
) -> Result<Vec<u8>, TapedriveError> {
let expected_chunk_size =
chunk_size(chunk_index, chunk_count, total_size).map_err(stream_error)?;
let mut chunk_data = vec![0u8; expected_chunk_size.as_usize()];
read_chunk_exact(reader, &mut chunk_data).await?;
Ok(chunk_data)
}
async fn read_chunk_exact<Reader: AsyncRead + Unpin>(
reader: &mut Reader,
chunk_data: &mut [u8],
) -> Result<(), TapedriveError> {
match reader.read_exact(chunk_data).await {
Ok(_) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => Err(stream_error(
StreamError::InvalidInput("stream ended before declared size".into()),
)),
Err(error) => Err(TapedriveError::Io(error)),
}
}
async fn verify_stream_drained<Reader: AsyncRead + Unpin>(
reader: &mut Reader,
) -> Result<(), TapedriveError> {
let mut extra = [0u8; 1];
if reader.read(&mut extra).await? != 0 {
return Err(stream_error(StreamError::InvalidInput(
"stream exceeded declared size".into(),
)));
}
Ok(())
}
async fn store_chunk<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
registered: RegisteredChunk,
) -> Result<PendingChunk, TapedriveError> {
let receipts = upload_with_retry(
client,
®istered.written,
®istered.plan,
Operation::WriteStream,
)
.await?;
Ok(PendingChunk {
entry: registered.entry,
written: registered.written,
receipts,
})
}
fn manifest_write_mode(name: &[u8], manifest_bytes: &[u8]) -> ManifestWriteMode {
if inline_write_fits(name, manifest_bytes.len()) {
ManifestWriteMode::Inline
} else {
ManifestWriteMode::Coded
}
}
async fn store_manifest<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
logical_size: StorageUnits,
manifest_bytes: &[u8],
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
if manifest_write_mode(name, manifest_bytes) == ManifestWriteMode::Inline {
let written = submit_raw_with_logical_size(
client,
tape_key,
name,
content_type,
logical_size,
manifest_bytes,
Operation::WriteStream,
)
.await?;
return Ok((written, Vec::new()));
}
let (written, plan) = submit_blob_with_logical_size(
client,
tape_key,
name,
content_type,
logical_size,
manifest_bytes,
Operation::WriteStream,
)
.await?;
let receipts = upload_with_retry(client, &written, &plan, Operation::WriteStream).await?;
Ok((written, receipts))
}
async fn store_manifest_for_chunks<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
name: &[u8],
content_type: ContentType,
size: StorageUnits,
pending_chunks: Vec<PendingChunk>,
) -> Result<(WrittenTrack, Vec<CertifyRes>), TapedriveError> {
let entries = pending_chunks
.into_iter()
.map(|pending_chunk| pending_chunk.entry)
.collect();
let manifest = build_manifest(hash(name), size, entries).map_err(stream_error)?;
let manifest_bytes = manifest.to_bytes().map_err(stream_error)?;
store_manifest(client, tape_key, name, content_type, size, &manifest_bytes).await
}
async fn complete_stream<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape_key: &impl TapeOperator,
manifest: &WrittenTrack,
receipts: &[CertifyRes],
) -> Result<StreamReceipt, TapedriveError> {
let track = if manifest.track.is_certified() {
manifest.track
} else {
certify_with_retry(
client,
tape_key,
manifest,
Operation::WriteStream,
receipts,
)
.await?
};
Ok(StreamReceipt::from_manifest_track(&track))
}
fn stream_error(error: StreamError) -> TapedriveError {
TapedriveError::Stream(error.to_string())
}
#[cfg(test)]
mod tests {
use bytemuck::Zeroable;
use tape_api::state::Tape;
use tape_core::types::{EpochNumber, StorageUnits, TapeNumber, TrackNumber};
use tape_crypto::address::Address;
use super::*;
fn make_tape(capacity_bytes: u64, used_bytes: u64, next_track_number: u64) -> Tape {
let mut tape = Tape::zeroed();
tape.id = TapeNumber(1);
tape.authority = Address::new_unique();
tape.capacity = StorageUnits::from_bytes(capacity_bytes);
tape.used = StorageUnits::from_bytes(used_bytes);
tape.active_epoch = EpochNumber(1);
tape.expiry_epoch = EpochNumber(2);
tape.tracks.next_number = TrackNumber(next_track_number);
tape
}
fn sample_manifest_bytes(chunk_count: u64) -> Vec<u8> {
let key = Hash::from([0x11; 32]);
let total_size = StorageUnits::from_bytes(MAX_TRACK_SIZE as u64 * chunk_count);
let entries = build_entries(TrackNumber(0), TrackNumber(chunk_count), total_size)
.expect("build entries");
build_manifest(key, total_size, entries)
.expect("build manifest")
.to_bytes()
.expect("serialize manifest")
}
#[test]
fn small_manifest() {
let manifest_bytes = sample_manifest_bytes(1);
assert_eq!(
manifest_write_mode(b"roms/small.bin", &manifest_bytes),
ManifestWriteMode::Inline
);
}
#[test]
fn large_manifest() {
let manifest_bytes = sample_manifest_bytes(64);
assert_eq!(
manifest_write_mode(b"roms/large.bin", &manifest_bytes),
ManifestWriteMode::Coded
);
}
#[test]
fn manifest_size() {
let key = Hash::from([0x11; 32]);
let total_size = StorageUnits::from_bytes(MAX_TRACK_SIZE as u64);
let entries = build_entries(TrackNumber(0), TrackNumber(1), total_size).expect("build entries");
let manifest = build_manifest(key, total_size, entries).expect("build manifest");
let manifest_bytes = manifest.to_bytes().expect("serialize manifest");
let total_required = total_size + StorageUnits::from_bytes(manifest_bytes.len() as u64);
let tape = make_tape(total_required.to_bytes() - 1, 0, 0);
let error = preflight(&tape, total_required, TrackNumber(2)).expect_err("preflight should fail");
match error {
TapedriveError::InsufficientCapacity { need, available } => {
assert_eq!(need, total_required);
assert_eq!(available.to_bytes(), total_required.to_bytes() - 1);
}
other => panic!("expected insufficient capacity, got {other}"),
}
}
#[test]
fn empty_stream() {
let error = validate_stream_size(StorageUnits::zero()).expect_err("empty stream should fail");
match error {
StreamError::InvalidInput(message) => {
assert_eq!(message, "empty streams are not supported");
}
other => panic!("expected empty stream error, got {other}"),
}
}
#[test]
fn overflow() {
let error = build_entries(
TrackNumber(u64::MAX),
TrackNumber(2),
StorageUnits::from_bytes(MAX_TRACK_SIZE as u64 * 2),
)
.expect_err("entries should fail");
match error {
StreamError::InvalidInput(message) => {
assert_eq!(message, "chunk track number overflow");
}
other => panic!("expected chunk track number overflow, got {other}"),
}
}
}