Pular para o conteúdo

4. Projeto: qualifique um ledger determinístico

O ledger determinístico armazena uma cadeia append-only SHA-256 em formato binário canônico. Seus módulos separam comandos, codec, armazenamento e modelo; vetores dourados fixam bytes exatos.

Qualifique como software de release. Compile com cache de pacotes limpo e verificado, execute testes, crie e acrescente registros e verifique o arquivo. Compare a mesma fixture entre dois builds limpos. Corrompa comprimento, payload e digest separadamente e confirme rejeição determinística sem perder o original. Examine endianness e limites no codec e limpeza após falhas no armazenamento.

Registre compilador, Longbow, alvo, comandos, checksums, saída e orçamentos de recursos. O resultado é uma qualificação reproduzível, não “funcionou uma vez”.

Exemplo testado

Qualifique um programa nativo multimódulo com seis testes e saída determinística.

Trecho em foco

curriculum/main-evidence/projects/ledger/src/model.fl:3–54

curriculum/main-evidence/projects/ledger/src/model.fl (excerpt)
export error LedgerError {
Usage,
InvalidFormat,
IntegrityFailure,
PayloadTooLarge
}
export structure Block {
private u64 blockIndex;
private [32]u8 previousDigest;
private string payloadValue;
private [32]u8 blockDigest;
};
Block Block::init(
u64 blockIndex,
[32]u8 previousDigest,
string payloadValue,
[32]u8 blockDigest
) override {
return build Block {
self.blockIndex = blockIndex;
self.previousDigest = previousDigest;
self.payloadValue = payloadValue;
self.blockDigest = blockDigest;
};
}
u64 Block::index(&const self) {
return self->blockIndex;
}
[]const u8 Block::previousHash(&const self) <-self {
return self->previousDigest[:];
}
stringv Block::payload(&const self) <-self {
return self->payloadValue.view();
}
[]const u8 Block::hash(&const self) <-self {
return self->blockDigest[:];
}
export [32]u8 copy_digest([]const u8 source) {
[32]u8 result = none;
loop usize index = 0; index < result.len(); index++ {
result[index] = source[index];
}
return result;
}

Este trecho depende dos outros arquivos do pacote. O código completo compilado está disponível abaixo.

Código completo do pacote (8 arquivos)

longbow.toml

longbow.toml
[package]
name = "ledger"
version = "0.1.0"
kind = "program"
[dependencies]

longbow.lock

longbow.lock
{
"schema": 1,
"root": "root",
"packages": [
{
"id": "root",
"identity": "local/ledger",
"source": {
"kind": "workspace",
"path": "."
},
"version": "0.1.0",
"dependencies": {
"std": "toolchain-std"
}
},
{
"id": "toolchain-std",
"identity": "flecha/std",
"source": {
"kind": "toolchain"
},
"dependencies": {}
}
]
}

src/codec.fl

src/codec.fl
module codec;
@imports
:: root::model
:: lib::std::bytes::buffer
:: lib::std::bytes::sequence
:: lib::std::containers::collections
:: lib::std::cryptography::sha2
:: lib::std::encoding::binary
:: lib::std::text::strings;
export const stringv FILE_HEADER = "FLECHA-LEDGER\n1\n";
const stringv HASH_DOMAIN = "FLECHA-LEDGER-BLOCK-V1\n";
const usize RECORD_HEADER_SIZE = 48;
const usize DIGEST_SIZE = 32;
const usize RECORD_OVERHEAD = RECORD_HEADER_SIZE + DIGEST_SIZE;
export (buffer::Buffer, [32]u8, error) encode_block(
u64 index,
[]const u8 previousHash,
stringv payload
) {
usize payloadLength = payload.len().cast<usize>;
usize capacity = RECORD_OVERHEAD + payloadLength;
if capacity < payloadLength or previousHash.len() != DIGEST_SIZE {
empty, allocationError := buffer::Buffer::withCapacity(0);
if allocationError != none {
return (empty, none, allocationError);
}
return (empty, none, model::LedgerError.PayloadTooLarge);
}
output, allocationError := buffer::Buffer::withCapacity(capacity);
if allocationError != none {
return (output, none, allocationError);
}
[48]u8 header = none;
headerWriter := binary::Writer::init(header[:]);
error writeError = headerWriter.writeBig<u64>(index);
if writeError == none {
writeError = headerWriter.writeBig<u64>(payloadLength.cast<u64>);
}
if writeError == none {
writeError = headerWriter.write(previousHash);
}
if writeError != none {
return (output, none, writeError);
}
digest, digestError := hash_block(header[:], payload);
if digestError != none {
return (output, none, digestError);
}
[32]u8 digestBytes = model::copy_digest(digest.bytes());
writeError = output.write(header[:]);
if writeError == none {
writeError = output.write(payload.cast<[]const u8>);
}
if writeError == none {
writeError = output.write(digestBytes[:]);
}
return (output, digestBytes, writeError);
}
(sha2::Sha256Digest, error) hash_block(
[]const u8 header,
stringv payload
) {
hasher := sha2::Sha256::init();
error digestError = hasher.update(HASH_DOMAIN.cast<[]const u8>);
if digestError == none {
digestError = hasher.update(header);
}
if digestError == none {
digestError = hasher.update(payload.cast<[]const u8>);
}
if digestError != none {
return (none, digestError);
}
return hasher.finalize();
}
export (collections::Array<model::Block>, error) parse(stringv contents) {
blocks := collections::Array<model::Block>::init();
if contents.len() < FILE_HEADER.len() or
contents.substr(0, FILE_HEADER.len()) != FILE_HEADER {
return (blocks, model::LedgerError.InvalidFormat);
}
source := contents.cast<[]const u8>;
reader := binary::Reader::init(source[FILE_HEADER.len().cast<usize>:]);
u64 expectedIndex = 0;
[32]u8 expectedPrevious = none;
while !reader.empty() {
if reader.remaining() < RECORD_OVERHEAD {
return (blocks, model::LedgerError.InvalidFormat);
}
index, indexError := reader.readBig<u64>();
payloadLength, lengthError := reader.readBig<u64>();
if indexError != none or lengthError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
[32]u8 previousValue = none;
{
previous, previousError := reader.read(DIGEST_SIZE);
if previousError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
previousValue = model::copy_digest(previous);
}
if payloadLength > (reader.remaining() - DIGEST_SIZE).cast<u64> {
return (blocks, model::LedgerError.InvalidFormat);
}
string payload = none;
{
payloadBytes, payloadError := reader.read(
payloadLength.cast<usize>
);
if payloadError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
error allocationError;
payload, allocationError = strings::string_from_bytes(payloadBytes);
if allocationError != none {
return (blocks, allocationError);
}
}
[32]u8 digestValue = none;
{
storedDigest, storedError := reader.read(DIGEST_SIZE);
if storedError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
digestValue = model::copy_digest(storedDigest);
}
if index != expectedIndex or
!sequence::constant_time_equal(
previousValue[:],
expectedPrevious[:]
) {
return (blocks, model::LedgerError.IntegrityFailure);
}
[48]u8 canonicalHeader = none;
canonicalWriter := binary::Writer::init(canonicalHeader[:]);
error canonicalError = canonicalWriter.writeBig<u64>(index);
if canonicalError == none {
canonicalError = canonicalWriter.writeBig<u64>(payloadLength);
}
if canonicalError == none {
canonicalError = canonicalWriter.write(previousValue[:]);
}
if canonicalError != none {
return (blocks, canonicalError);
}
calculated, digestError := hash_block(
canonicalHeader[:],
payload.view()
);
if digestError != none {
return (blocks, digestError);
}
if !sequence::constant_time_equal(calculated.bytes(), digestValue[:]) {
return (blocks, model::LedgerError.IntegrityFailure);
}
pushError := blocks.push(model::Block::init(
index,
previousValue,
payload,
digestValue
));
if pushError != none {
return (blocks, pushError);
}
expectedPrevious = digestValue;
expectedIndex++;
if expectedIndex == 0 and !reader.empty() {
return (blocks, model::LedgerError.InvalidFormat);
}
}
return (blocks, none);
}

src/codec_test.fl

src/codec_test.fl
module codec_test;
@imports
:: root::codec
:: root::model
:: lib::std::bytes::buffer
:: lib::std::bytes::sequence
:: lib::std::encoding::hex
:: lib::std::tests::asserts
:: lib::std::tests::testing
:: lib::std::text::strings;
const stringv ALPHA_DIGEST =
"aba5ed27f8ee302525deccc60adfb950fd3d71699082bb3b5c0bcad6a36ec139";
const stringv BETA_DIGEST =
"d4298ba679357d6f92c5ea1da21f8895a3e6d514b35e51b3e68081744d9f6b4a";
const stringv ALPHA_RECORD =
"000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000616c706861aba5ed27f8ee302525deccc60adfb950fd3d71699082bb3b5c0bcad6a36ec139";
test canonical_block_matches_published_golden_vector(&testing::UnitTest t) {
[32]u8 genesis = none;
record, digest, encodeError := codec::encode_block(
0,
genesis[:],
"alpha"
);
asserts::is_true(t, encodeError == none, "golden block encodes");
if encodeError != none {
return;
}
recordHex, recordHexError := hex::encode_bytes(record.bytes());
digestHex, digestHexError := hex::encode_bytes(digest[:]);
asserts::is_true(
t,
recordHexError == none and recordHex.view() == ALPHA_RECORD,
"canonical record bytes match the golden vector"
);
asserts::is_true(
t,
digestHexError == none and digestHex.view() == ALPHA_DIGEST,
"domain-separated block hash matches the golden vector"
);
}
test chain_round_trips_and_links_every_block(&testing::UnitTest t) {
chain, chainError := make_chain("alpha", "beta");
asserts::is_true(t, chainError == none, "chain construction succeeds");
if chainError != none {
return;
}
blocks, parseError := codec::parse(chain.view());
asserts::is_true(t, parseError == none, "canonical chain parses");
asserts::is_true(t, blocks.len() == 2, "both blocks reload");
if parseError != none or blocks.len() != 2 {
return;
}
values := blocks.view();
firstDigest, firstHexError := hex::encode_bytes(values[0].hash());
secondDigest, secondHexError := hex::encode_bytes(values[1].hash());
asserts::is_true(
t,
values[0].index() == 0 and values[0].payload() == "alpha",
"the genesis block preserves its fields"
);
asserts::is_true(
t,
values[1].index() == 1 and values[1].payload() == "beta",
"the second block preserves its fields"
);
asserts::is_true(
t,
firstHexError == none and firstDigest.view() == ALPHA_DIGEST,
"genesis digest is stable"
);
asserts::is_true(
t,
secondHexError == none and secondDigest.view() == BETA_DIGEST,
"head digest is stable"
);
asserts::is_true(
t,
sequence::equal(values[1].previousHash(), values[0].hash()),
"the second block links to the genesis digest"
);
}
test identical_chains_produce_identical_bytes(&testing::UnitTest t) {
first, firstError := make_chain("alpha", "beta");
second, secondError := make_chain("alpha", "beta");
asserts::is_true(
t,
firstError == none and secondError == none and
first.view() == second.view(),
"the canonical chain is reproducible"
);
}
test malformed_and_truncated_storage_is_rejected(&testing::UnitTest t) {
malformed, malformedError := codec::parse("not-a-ledger");
asserts::is_true(
t,
malformedError == model::LedgerError.InvalidFormat and
malformed.empty(),
"a malformed header is rejected"
);
chain, chainError := make_chain("alpha", "beta");
if chainError != none {
t->fail();
return;
}
truncated, truncatedError := codec::parse(
chain.view().substr(0, chain.len() - 1)
);
asserts::is_true(
t,
truncatedError == model::LedgerError.InvalidFormat and
truncated.len() == 1,
"a truncated final digest is rejected after the valid prefix"
);
}
test tampered_payload_and_digest_are_rejected(&testing::UnitTest t) {
chain, chainError := make_chain("alpha", "beta");
if chainError != none {
t->fail();
return;
}
tamperedPayload, payloadError := changed_byte(chain.view(), 64, 'A');
if payloadError != none {
t->fail();
return;
}
_, tamperError := codec::parse(tamperedPayload.view());
asserts::is_true(
t,
tamperError == model::LedgerError.IntegrityFailure,
"payload tampering is detected"
);
tamperedDigest, digestError := changed_byte(chain.view(), 84, 0);
if digestError != none {
t->fail();
return;
}
_, storedDigestError := codec::parse(tamperedDigest.view());
asserts::is_true(
t,
storedDigestError == model::LedgerError.IntegrityFailure,
"stored digest tampering is detected"
);
}
test reordered_blocks_are_rejected(&testing::UnitTest t) {
chain, chainError := make_chain("alpha", "bravo");
if chainError != none {
t->fail();
return;
}
source := chain.view().cast<[]const u8>;
reordered, allocationError := buffer::Buffer::withCapacity(source.len());
if allocationError != none or
reordered.write(codec::FILE_HEADER.cast<[]const u8>) != none or
reordered.write(source[101:186]) != none or
reordered.write(source[16:101]) != none {
t->fail();
return;
}
reorderedText, textError := strings::string_from_bytes(reordered.bytes());
if textError != none {
t->fail();
return;
}
_, parseError := codec::parse(reorderedText.view());
asserts::is_true(
t,
parseError == model::LedgerError.IntegrityFailure,
"record reordering breaks the index and hash chain"
);
}
(string, error) make_chain(stringv firstPayload, stringv secondPayload) {
output, allocationError := buffer::Buffer::withCapacity(256);
if allocationError != none {
return (none, allocationError);
}
error writeError = output.write(codec::FILE_HEADER.cast<[]const u8>);
if writeError != none {
return (none, writeError);
}
[32]u8 genesis = none;
first, firstDigest, firstError := codec::encode_block(
0,
genesis[:],
firstPayload
);
if firstError != none {
return (none, firstError);
}
second, _, secondError := codec::encode_block(
1,
firstDigest[:],
secondPayload
);
if secondError != none {
return (none, secondError);
}
writeError = output.write(first.bytes());
if writeError == none {
writeError = output.write(second.bytes());
}
if writeError != none {
return (none, writeError);
}
return strings::string_from_bytes(output.bytes());
}
(string, error) changed_byte(stringv source, usize offset, u8 value) {
bytes, allocationError := new<own []u8>(source.len().cast<usize>);
if allocationError != none {
return (none, allocationError);
}
loop usize index = 0; index < bytes.len(); index++ {
bytes[index] = source[index.cast<u32>];
}
bytes[offset] = value;
return strings::string_from_bytes(bytes[:]);
}

src/commands.fl

src/commands.fl
module commands;
@imports
:: root::storage
:: lib::std::encoding::hex
:: lib::std::fmt;
export error create(stringv path) {
result := storage::create(path);
if result == none {
fmt::println("created: 0 blocks");
}
return result;
}
export error append(stringv path, stringv payload) {
index, digest, appendError := storage::append(path, payload);
if appendError != none {
return appendError;
}
fmt::print("appended: ");
fmt::print(index);
fmt::print(" ");
fmt::println(digest.view());
return none;
}
export error verify(stringv path) {
blocks, loadError := storage::load(path);
if loadError != none {
return loadError;
}
fmt::print("verified: ");
fmt::print(blocks.len());
fmt::print(" blocks");
if !blocks.empty() {
values := blocks.view();
head, hexError := hex::encode_bytes(values[values.len() - 1].hash());
if hexError != none {
return hexError;
}
fmt::print(" ");
fmt::print(head.view());
}
fmt::println("");
return none;
}
export error list(stringv path) {
blocks, loadError := storage::load(path);
if loadError != none {
return loadError;
}
values := blocks.view();
loop usize index = 0; index < values.len(); index++ {
digest, digestError := hex::encode_bytes(values[index].hash());
if digestError != none {
return digestError;
}
payload, payloadError := hex::encode(values[index].payload());
if payloadError != none {
return payloadError;
}
fmt::print(values[index].index());
fmt::print(" ");
fmt::print(digest.view());
fmt::print(" ");
fmt::println(payload.view());
}
return none;
}

src/main.fl

src/main.fl
module main;
@imports
:: root::commands
:: root::model
:: lib::std::fmt
:: lib::std::os::arguments;
i32 main() {
values, captureError := arguments::capture();
if captureError != none {
fmt::eprintln("ledger: cannot capture command-line arguments");
return 3;
}
if values.len() < 2 {
print_usage();
return 2;
}
command := values[1].view();
error result = none;
if command == "create" and values.len() == 3 {
result = commands::create(values[2].view());
} else if command == "append" and values.len() == 4 {
result = commands::append(values[2].view(), values[3].view());
} else if command == "verify" and values.len() == 3 {
result = commands::verify(values[2].view());
} else if command == "list" and values.len() == 3 {
result = commands::list(values[2].view());
} else if command == "help" or command == "--help" or command == "-h" {
print_usage();
return 0;
} else {
result = model::LedgerError.Usage;
}
if result == none {
return 0;
}
if result == model::LedgerError.Usage {
print_usage();
return 2;
}
if result == model::LedgerError.InvalidFormat or
result == model::LedgerError.PayloadTooLarge {
fmt::eprintln("ledger: invalid ledger format");
return 4;
}
if result == model::LedgerError.IntegrityFailure {
fmt::eprintln("ledger: integrity verification failed");
return 5;
}
fmt::eprintln("ledger: operation failed");
return 3;
}
void print_usage() {
fmt::println("ledger - deterministic append-only SHA-256 chain");
fmt::println("");
fmt::println("usage:");
fmt::println(" ledger create <file>");
fmt::println(" ledger append <file> <payload>");
fmt::println(" ledger verify <file>");
fmt::println(" ledger list <file>");
}

src/model.fl

src/model.fl
module model;
export error LedgerError {
Usage,
InvalidFormat,
IntegrityFailure,
PayloadTooLarge
}
export structure Block {
private u64 blockIndex;
private [32]u8 previousDigest;
private string payloadValue;
private [32]u8 blockDigest;
};
Block Block::init(
u64 blockIndex,
[32]u8 previousDigest,
string payloadValue,
[32]u8 blockDigest
) override {
return build Block {
self.blockIndex = blockIndex;
self.previousDigest = previousDigest;
self.payloadValue = payloadValue;
self.blockDigest = blockDigest;
};
}
u64 Block::index(&const self) {
return self->blockIndex;
}
[]const u8 Block::previousHash(&const self) <-self {
return self->previousDigest[:];
}
stringv Block::payload(&const self) <-self {
return self->payloadValue.view();
}
[]const u8 Block::hash(&const self) <-self {
return self->blockDigest[:];
}
export [32]u8 copy_digest([]const u8 source) {
[32]u8 result = none;
loop usize index = 0; index < result.len(); index++ {
result[index] = source[index];
}
return result;
}

src/storage.fl

src/storage.fl
module storage;
@imports
:: root::codec
:: root::model
:: lib::std::containers::collections
:: lib::std::encoding::hex
:: lib::std::io::file
:: lib::std::io::filesystem;
export error create(stringv path) {
options := build file::OpenOptions {
self.write = true;
self.createNew = true;
};
output, openError := file::File::open(path, options);
if openError != none {
return openError;
}
error result = output.writeAll(codec::FILE_HEADER.cast<[]const u8>);
if result == none {
result = output.flush();
}
closeError := output.close();
if result == none {
result = closeError;
}
if result != none {
error _ = filesystem::remove_file(path);
}
return result;
}
export (collections::Array<model::Block>, error) load(stringv path) {
contents, readError := filesystem::read_file(path);
if readError != none {
return (collections::Array<model::Block>::init(), readError);
}
return codec::parse(contents.view());
}
export (u64, string, error) append(stringv path, stringv payload) {
blocks, loadError := load(path);
if loadError != none {
return (none, none, loadError);
}
u64 index = blocks.len().cast<u64>;
[32]u8 previous = none;
if !blocks.empty() {
values := blocks.view();
previous = model::copy_digest(values[values.len() - 1].hash());
}
record, digest, encodeError := codec::encode_block(
index,
previous[:],
payload
);
if encodeError != none {
return (none, none, encodeError);
}
output, openError := file::File::open(path, file::OpenOptions::appendOnly());
if openError != none {
return (none, none, openError);
}
error result = output.writeAll(record.bytes());
if result == none {
result = output.flush();
}
closeError := output.close();
if result == none {
result = closeError;
}
if result != none {
return (none, none, result);
}
digestText, hexError := hex::encode_bytes(digest[:]);
return (index, digestText, hexError);
}

O trecho do modelo define os registros compartilhados por codec, armazenamento e comandos. O pacote completo e os seis testes são mostrados porque o determinismo depende da serialização e do comportamento de arquivos combinados.

Comandos

Comandos
longbow check curriculum/main-evidence/projects/ledger
longbow test curriculum/main-evidence/projects/ledger
longbow build curriculum/main-evidence/projects/ledger
longbow run curriculum/main-evidence/projects/ledger -- help

Saída esperada

Saída esperada
codec_test.fl ...................................... OK [6/6] tests passed
ledger - deterministic append-only SHA-256 chain
usage:
ledger create <file>
ledger append <file> <payload>
ledger verify <file>
ledger list <file>
status de saída: 0

Referência relacionada: /examples/ledger/src/model/.

Continue em Unsafe: o limite unsafe somente quando precisar implementar uma fronteira que Flecha segura não expressa.