Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/add/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ func (c *countingAddCAS) Get(ctx context.Context, key cid.Cid) ([]byte, error) {
return c.inner.Get(ctx, key)
}

func (g *countingAddGateway) CreateMeasuredPayload(context.Context, []cid.Cid, uint64, uint64) (cid.Cid, error) {
func (g *countingAddGateway) CreatePositionalPayload(context.Context, []cid.Cid, cid.Cid) (cid.Cid, error) {
g.calls++
return cid.Undef, os.ErrInvalid
}
Expand Down
3 changes: 2 additions & 1 deletion application/add/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package add
import (
"context"
"fmt"

"github.com/dewebprotocol/malt-client/unixfs"
"github.com/dewebprotocol/malt-core/maltcid"
)

// projectionWriter belongs to the UnixFS layout compiler, not the transport.
type projectionWriter interface {
unixfs.StagedRootWriter
unixfs.MeasuredPayloadWriter
unixfs.PositionalPayloadWriter
}

func newProjectionWriter(ctx context.Context, remote Materializer, layout unixfs.LayoutKind) (*unixfs.AuthenticationAdapter, error) {
Expand Down
6 changes: 4 additions & 2 deletions docs/authentication-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ stays in `unixfs/model`; AuthenticationSteps produces explicit component labels.
A full-path index instead submits one opaque full-path label. A Root's AA, not
slash syntax inferred by the generic engine, chooses the conversion rule.
The current UnixFS planner builds Prefix and Positional V0 Roots directly
through typed authentication. No semantic Map/List adapter remains. Only Prefix supports system bindings; Positional
metadata is structural. New low-level clients can choose native 32-byte keys
through typed authentication. No semantic Map/List adapter remains. Prefix
payload labels are ordinary application bindings. Positional root metadata
contains the sequence count and an optional opaque payload CID; the application
verifies and decodes the referenced document. New low-level clients can choose native 32-byte keys
or a registered label rule explicitly.

Writeback preserves the base Root's layout, AA and exact VC profile. Historical
Expand Down
34 changes: 34 additions & 0 deletions docs/positional-chunk-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Positional chunk metadata

A Positional Root authenticates its element count and an optional payload CID.
Core does not interpret file sizes, chunk widths or application JSON. UnixFS
requires a payload CID for chunked files and uses the following canonical UTF-8
JSON document, encoded as a raw CID:

```json
{"profile":"malt.unixfs.chunks/1","chunk_size":"4","total_size":"8"}
```

Fields occur in the order shown, without whitespace or extra fields. Sizes are
canonical unsigned decimal strings. `chunk_size` must be positive. The count
already authenticated by Core must equal zero for an empty file, otherwise
`1 + (total_size - 1) / chunk_size`. The document does not duplicate count.
`unixfs/model` owns encoding and validation. Identity CIDs may inline the same
bytes; other CIDs are fetched through the injected block reader and hashed
before decoding.

Writers upload this document with the chunk blocks, verify the returned CID,
and pass that CID to `CreatePositionalPayload`. The document consumes no data
index. Readers authenticate the root metadata, verify the JSON CID and count,
translate the requested half-open byte interval into a half-open element-index
interval, and verify that range against the same Root. Every returned chunk is
CID-checked, length-checked, then sliced locally. Empty files retain their
metadata document. Encrypted files use ciphertext sizes in this document and
check them against their authenticated encryption manifest.

A payload CID is also a graph dependency: write plans retain its block or Root,
place referenced candidates before their parents, and include it in dependency
closure. It does not accept a trusted Root or authorize publication.

The structural layout and proof format are defined only by the
[Core specification](../../malt-core/docs/spec/authentication-contracts.md).
18 changes: 11 additions & 7 deletions docs/typed-authentication-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

The runtime uses the sole current Core path: `derivation`, `auth/tree`,
`engine`, explicit traversal, and `sdk/authentication`. Native HTTP queries
use `malt.authentication/3`; candidates use `malt.authentication/2`. Ordered
writes use `malt.authentication-batch/1` and exact
`malt.authentication-receipt/1` checks. No old Resolve/Read, Map-proof,
use `malt.authentication/5`; candidates use `malt.authentication/4`. Ordered
writes use `malt.authentication-batch/2` and exact
`malt.authentication-receipt/2` checks. No old Resolve/Read, Map-proof,
UpdateView, semantic intent, client-root bundle, or WASM fallback is used.

## Reads and application layouts
Expand All @@ -16,8 +16,11 @@ target remains a Prefix Root, a content read explicitly authenticates its
ordinary `@payload` label. All directory layouts use public SHA256 derivation
(profile 4); ReaderOptions and filesystem service options carry the application
layout. Managed mounts obtain it from the selected Bucket metadata.
Positional chunk Roots expose authenticated geometry and byte ranges, with no
system payload binding.
Positional Roots authenticate `count` and an optional `payload_cid` in root
slot zero. Non-root vectors use every slot. UnixFS stores byte geometry in
CID-bound application JSON, verifies that document against the authenticated
count, and converts byte ranges to Core element-index intervals. See
[chunk metadata](positional-chunk-metadata.md).

`unixfs.AuthenticationAdapter` computes Roots locally and checks exact remote
candidate identity. The verified reader checks each caller-selected typed
Expand Down Expand Up @@ -53,7 +56,8 @@ new versioned measurement fields. Source workspace validation is not release
publication. The runtime module namespace/tag gate and exact downstream
release pins remain separate from this source migration.

Native builds pin published Core `v0.0.10-rc.3` at commit
`6f16b13f3abe9cbd0992e83e1457663935e5a72f` for independent module builds.
Native builds pin Core commit `949fb4e690c81d7ff1b5141ec95ddf7f7a926460`
through its exact Go pseudo-version for independent module builds. This source
integration is not a runtime or browser release.
New roots use the remote's advertised backend as an
untrusted creation hint; updates preserve the existing Root descriptor.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.26.0
require (
github.com/Microsoft/go-winio v0.6.2
github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf
github.com/dewebprotocol/malt-core v0.0.10-rc.4
github.com/dewebprotocol/malt-core v0.0.10-rc.4.0.20261009112445-949fb4e690c8
github.com/hanwen/go-fuse/v2 v2.11.0
github.com/ipfs/boxo v0.41.0
github.com/ipfs/go-block-format v0.2.4
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dewebprotocol/malt-core v0.0.10-rc.4 h1:BpA7vH0LwKTmipTlAUu97CniBXTxBT9f5AQWV8UFbmY=
github.com/dewebprotocol/malt-core v0.0.10-rc.4/go.mod h1:o6L8K9vJp5hyrVo3fUtSsj0rvNpcboOcOb3nQwnR/gs=
github.com/dewebprotocol/malt-core v0.0.10-rc.4.0.20261009112445-949fb4e690c8 h1:Eqvk2/jmKg48S0My2/QYRG/3jc5sEEMFsW4iEAvHE3A=
github.com/dewebprotocol/malt-core v0.0.10-rc.4.0.20261009112445-949fb4e690c8/go.mod h1:o6L8K9vJp5hyrVo3fUtSsj0rvNpcboOcOb3nQwnR/gs=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
Expand Down
27 changes: 19 additions & 8 deletions internal/evaluation/authenticationgraph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ func (s *Session) fetch(ctx context.Context, root cid.Cid) (*graph, LoadMetrics,
if err != nil {
return fmt.Errorf("authenticate candidate %s: %w", key, err)
}
for _, entry := range candidate.State.Entries {
if _, _, err := maltcid.ParseRoot(entry.Target); err == nil {
if err := visit(entry.Target, depth+1); err != nil {
for _, target := range graphTargets(candidate.State) {
if _, _, err := maltcid.ParseRoot(target); err == nil {
if err := visit(target, depth+1); err != nil {
return err
}
heights[key] = max(heights[key], 1+heights[entry.Target.String()])
heights[key] = max(heights[key], 1+heights[target.String()])
}
}
next.objects[key] = object{writer: writer, state: cloneState(candidate.State), bytes: size}
Expand Down Expand Up @@ -326,12 +326,12 @@ func (e *Edit) graph(ctx context.Context, root cid.Cid) (*graph, error) {
entries += uint64(len(o.state.Entries))
size += o.bytes
visiting[key] = true
for _, entry := range o.state.Entries {
if _, _, err := maltcid.ParseRoot(entry.Target); err == nil {
if err := visit(entry.Target, depth+1); err != nil {
for _, target := range graphTargets(o.state) {
if _, _, err := maltcid.ParseRoot(target); err == nil {
if err := visit(target, depth+1); err != nil {
return err
}
heights[key] = max(heights[key], 1+heights[entry.Target.String()])
heights[key] = max(heights[key], 1+heights[target.String()])
}
}
next.objects[key] = o
Expand Down Expand Up @@ -391,3 +391,14 @@ func cloneState(state engine.State) engine.State {
return state
}
func elapsed(started time.Time) uint64 { return uint64(max(time.Since(started), 0)) }

func graphTargets(state engine.State) []cid.Cid {
targets := make([]cid.Cid, 0, len(state.Entries)+1)
if state.PayloadCID.Defined() {
targets = append(targets, state.PayloadCID)
}
for _, entry := range state.Entries {
targets = append(targets, entry.Target)
}
return targets
}
2 changes: 1 addition & 1 deletion internal/evaluation/authenticationgraph/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func graphFixture(t *testing.T) (*Session, *fixtureRemote, *countedScheme, cid.C
}
e := engine.New(registry)
payload := raw(t, "first chunk")
child, err := authentication.Prepare(ctx, e, engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: maltcid.KZG4096}, Entries: []engine.Entry{{Label: coordinate.EncodeIndex(0), Target: payload}}, ChunkSize: 11, TotalSize: 11})
child, err := authentication.Prepare(ctx, e, engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: maltcid.KZG4096}, Entries: []engine.Entry{{Label: coordinate.EncodeIndex(0), Target: payload}}})
if err != nil {
t.Fatal(err)
}
Expand Down
7 changes: 6 additions & 1 deletion internal/evaluation/rq2fixture/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

clientcas "github.com/dewebprotocol/malt-client/internal/cas"
"github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph"
unixfsmodel "github.com/dewebprotocol/malt-client/unixfs/model"
"github.com/dewebprotocol/malt-core/auth/coordinate"
"github.com/dewebprotocol/malt-core/derivation"
"github.com/dewebprotocol/malt-core/engine"
Expand Down Expand Up @@ -36,7 +37,11 @@ func (s *SourceDefinition) Candidates(ctx context.Context, e *engine.Engine, bac
bindings = append(bindings, engine.Entry{Label: []byte(file.Path), Target: target})
}
for _, file := range s.ListFiles {
state := engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: profile}, ChunkSize: file.ChunkSize, TotalSize: file.TotalSize, Entries: make([]engine.Entry, len(file.Chunks))}
state := engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: profile}, Entries: make([]engine.Entry, len(file.Chunks))}
state.PayloadCID, err = unixfsmodel.InlineChunkMetadata(file.TotalSize, file.ChunkSize, uint64(len(file.Chunks)))
if err != nil {
return nil, err
}
for i, chunk := range file.Chunks {
target, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: chunk.Bytes})
if err != nil {
Expand Down
22 changes: 14 additions & 8 deletions internal/evaluation/rq2fixture/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

clientcas "github.com/dewebprotocol/malt-client/internal/cas"
"github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph"
unixfsmodel "github.com/dewebprotocol/malt-client/unixfs/model"
"github.com/dewebprotocol/malt-core/auth/coordinate"
"github.com/dewebprotocol/malt-core/derivation"
"github.com/dewebprotocol/malt-core/engine"
Expand Down Expand Up @@ -32,7 +33,7 @@ func (f *Fixture) ValidateGraphAgainstSource(view authenticationgraph.View, back
if err != nil {
return err
}
if root.Descriptor != descriptor || root.ChunkSize != 0 || root.TotalSize != 0 || len(root.Entries) != len(source) {
if root.Descriptor != descriptor || root.PayloadCID.Defined() || len(root.Entries) != len(source) {
return fmt.Errorf("root metadata or binding count differs from source")
}
entries := make(map[string]engine.Entry, len(root.Entries))
Expand Down Expand Up @@ -71,7 +72,11 @@ func (f *Fixture) ValidateGraphAgainstSource(view authenticationgraph.View, back
if err != nil {
return err
}
if object.Descriptor != descriptor || object.ChunkSize != file.ChunkSize || object.TotalSize != uint64(len(data)) {
geometry, err := unixfsmodel.ParseInlineChunkMetadata(object.PayloadCID, uint64(len(object.Entries)))
if err != nil {
return err
}
if object.Descriptor != descriptor || geometry.ChunkSize != file.ChunkSize || geometry.TotalSize != uint64(len(data)) {
return fmt.Errorf("source file %q measured metadata does not match post-image", path)
}
if err := validateChunkEntries(object, data); err != nil {
Expand All @@ -90,11 +95,12 @@ func (f *Fixture) ValidateGraphAgainstSource(view authenticationgraph.View, back
return nil
}
func validateChunkEntries(state engine.State, data []byte) error {
if state.ChunkSize == 0 {
return fmt.Errorf("chunk size is zero")
geometry, err := unixfsmodel.ParseInlineChunkMetadata(state.PayloadCID, uint64(len(state.Entries)))
if err != nil {
return err
}
count := uint64(len(data)) / state.ChunkSize
if uint64(len(data))%state.ChunkSize != 0 {
count := uint64(len(data)) / geometry.ChunkSize
if uint64(len(data))%geometry.ChunkSize != 0 {
count++
}
if uint64(len(state.Entries)) != count {
Expand All @@ -109,8 +115,8 @@ func validateChunkEntries(state engine.State, data []byte) error {
return fmt.Errorf("chunk input is duplicate, invalid or outside the measured sequence")
}
seen[index] = true
start := index * state.ChunkSize
end := start + min(uint64(len(data))-start, state.ChunkSize)
start := index * geometry.ChunkSize
end := start + min(uint64(len(data))-start, geometry.ChunkSize)
expected, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: data[start:end]})
if err != nil || !entry.Target.Equals(expected) {
return fmt.Errorf("chunk %d CID does not bind its source bytes", index)
Expand Down
19 changes: 14 additions & 5 deletions internal/evaluation/rq2write/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph"
"github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture"
unixfsmodel "github.com/dewebprotocol/malt-client/unixfs/model"
"github.com/dewebprotocol/malt-core/auth/coordinate"
"github.com/dewebprotocol/malt-core/derivation"
"github.com/dewebprotocol/malt-core/engine"
Expand Down Expand Up @@ -41,26 +42,34 @@ func Apply(ctx context.Context, edit *authenticationgraph.Edit, root cid.Cid, op
if err != nil {
return cid.Undef, err
}
if child.Descriptor.Layout != maltcid.Positional || child.Descriptor.DerivationProfile != uint8(derivation.Direct) || child.Descriptor.Profile != state.Descriptor.Profile || child.ChunkSize == 0 {
if child.Descriptor.Layout != maltcid.Positional || child.Descriptor.DerivationProfile != uint8(derivation.Direct) || child.Descriptor.Profile != state.Descriptor.Profile || !child.PayloadCID.Defined() {
return cid.Undef, fmt.Errorf("file requires a measured Positional Root with the parent's VC profile")
}
if len(payloads) != 1 {
return cid.Undef, fmt.Errorf("chunk operation requires one payload")
}
geometry, err := unixfsmodel.ParseInlineChunkMetadata(child.PayloadCID, uint64(len(child.Entries)))
if err != nil {
return cid.Undef, err
}
var delta authentication.Delta
if operation.Kind == rq2fixture.KindListAppend {
if child.TotalSize%child.ChunkSize != 0 || operation.PayloadBytes != child.ChunkSize || child.TotalSize > math.MaxUint64-child.ChunkSize {
if geometry.TotalSize%geometry.ChunkSize != 0 || operation.PayloadBytes != geometry.ChunkSize || geometry.TotalSize > math.MaxUint64-geometry.ChunkSize {
return cid.Undef, fmt.Errorf("append requires a full final chunk and one same-width new chunk")
}
count := uint64(len(child.Entries)) + 1
total := child.TotalSize + child.ChunkSize
delta = authentication.Delta{Changes: []engine.Change{{Label: coordinate.EncodeIndex(count - 1), After: payloads[0]}}, Count: &count, TotalSize: &total}
total := geometry.TotalSize + geometry.ChunkSize
payload, err := unixfsmodel.InlineChunkMetadata(total, geometry.ChunkSize, count)
if err != nil {
return cid.Undef, err
}
delta = authentication.Delta{Changes: []engine.Change{{Label: coordinate.EncodeIndex(count - 1), After: payloads[0]}}, Count: &count, PayloadCID: &payload}
} else {
if operation.ListIndex == nil || *operation.ListIndex >= uint64(len(child.Entries)) {
return cid.Undef, fmt.Errorf("replacement index is outside measured file")
}
index := *operation.ListIndex
size := min(child.ChunkSize, child.TotalSize-index*child.ChunkSize)
size := min(geometry.ChunkSize, geometry.TotalSize-index*geometry.ChunkSize)
if operation.PayloadBytes != size {
return cid.Undef, fmt.Errorf("replacement must preserve the authenticated chunk length")
}
Expand Down
6 changes: 3 additions & 3 deletions unixfs/authentication_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

// AuthenticationAdapter compiles the selected UnixFS layout to typed ArcSets.
// Directory payloads are system bindings; names are application labels. File
// block sequences carry measured structural metadata and no system payload.
// block sequences bind application metadata through an opaque root payload CID.
// It computes candidates locally and checks the exact remote receipt.
type AuthenticationAdapter struct {
remote nodeapi.AuthenticationWriter
Expand Down Expand Up @@ -124,8 +124,8 @@ func (a *AuthenticationAdapter) materialize(ctx context.Context, previous cid.Ci
}
return expected, nil
}
func (a *AuthenticationAdapter) CreateMeasuredPayload(ctx context.Context, chunks []cid.Cid, total, chunkSize uint64) (cid.Cid, error) {
state := engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: a.profile}, ChunkSize: chunkSize, TotalSize: total, Entries: make([]engine.Entry, len(chunks))}
func (a *AuthenticationAdapter) CreatePositionalPayload(ctx context.Context, chunks []cid.Cid, payload cid.Cid) (cid.Cid, error) {
state := engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: a.profile}, PayloadCID: payload, Entries: make([]engine.Entry, len(chunks))}
for i, target := range chunks {
state.Entries[i] = engine.Entry{Label: coordinate.EncodeIndex(uint64(i)), Target: target}
}
Expand Down
Loading
Loading