store

package
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package store is how a record commits to a set of other records and how a reader opens that set: the refs entry that names a store and commits to its root, the manifest that lists a store's members, and the rule that computes the root from the entry and the members' commitments.

The entry and the manifest are the same for every application that stores records this way, so they live here. Where an entry sits in a record (which key, which kinds may carry one) and how large a manifest body may be belong to the application: the entry codec works on the array value alone, and Body takes the bound.

A refusal that wraps a sentinel starts with that sentinel's text, so an application that words its own refusals can swap the prefix for its own and keep the rest.

Index

Examples

Constants

View Source
const MaxMembers = 1024

MaxMembers bounds a manifest's member list, so the work one manifest can ask of a reader, a fetch and a check per member, is bounded by the manifest. A store that needs more members than this should hold locators rather than content.

View Source
const MaxRefMembers = 8

MaxRefMembers bounds one refs entry, so an entry cannot carry unbounded material under names this version does not define. Four are defined; the rest is room for the format to grow without orphaning a reader.

View Source
const MaxRefName = 64

MaxRefName bounds a store name, and a manifest member's name and type.

View Source
const MaxRefs = 64

MaxRefs bounds how many stores one record may commit to. Without it a record is an instruction to a reader to make an unbounded number of requests, because a reader reads every store a record links. Generous for a directory entry and small enough that the work a record can ask for is bounded by the record.

View Source
const MemberKey = "members"

MemberKey is the body key a manifest's member list lives under. A manifest's body is the whole of its record, so this cannot collide with a publisher's own field the way a reserved name in a profile would.

Variables

View Source
var (
	// ErrNotManifest is a body that is not a member list.
	ErrNotManifest = errors.New("store: not a manifest body")
	// ErrMemberCount is a manifest with no members or more than MaxMembers.
	ErrMemberCount = errors.New("store: manifest member count out of range")
)
View Source
var (
	// ErrField is an entry or a member of the wrong type, width or length.
	ErrField = errors.New("store: field has the wrong shape")
	// ErrDupRef is two entries naming one store.
	ErrDupRef = errors.New("store: two refs entries name the same store")
)
View Source
var ErrNoHead = errors.New("no head")

ErrNoHead is a ref that commits to a store without naming a member of it. Not a fault: the publisher may intend the store to be found some other way, and a reader has nothing to ask a host for.

View Source
var ErrUnsupported = errors.New("unsupported store")

ErrUnsupported is a store whose entry carries a member this version does not define. The record is fine; this one store cannot be read here.

View Source
var MemberOverhead = func() int {
	one, err := (&Manifest{Members: []Member{{}}}).list()
	if err != nil {
		panic(err)
	}
	empty, err := cbor.Encode(cbor.Map{{Key: MemberKey, Val: []cbor.Value{}}})
	if err != nil {
		panic(err)
	}
	b, err := cbor.Encode(one)
	if err != nil {
		panic(err)
	}
	return len(b) - len(empty)
}()

MemberOverhead is the encoded cost of one member entry excluding its name and type, measured rather than assumed: the fixed-width map, its four keys, the 32-byte commitment and a size. It is what lets a caller work out how many members a manifest can hold before it mints any of them, since MaxMembers alone is reachable only for short names.

It is a variable because it is computed from the encoder when the package loads, so it cannot disagree with what Body encodes. Callers read it and must not assign to it. Every package in the process reads the same value, and a changed one would have each of them size manifests by a cost the encoder does not have: more members than Body accepts, or fewer than fit.

Functions

func EncodeRefs

func EncodeRefs(in []Ref) ([]cbor.Value, error)

EncodeRefs writes a record's refs entries as the array value the record carries, checking every entry's shape.

func Head(ref Ref) ([32]byte, error)

Head is the commitment a reader asks the host for to open a store: the one member when the store has one, the manifest when it has more. It refuses a ref that names no head, which is a store committed to but not linked, and a count of zero, which commits to nothing.

func Root

func Root(ref Ref, members [][32]byte) [32]byte

Root is the root a ref commits to. A store of one member is its own head and its root is the leaf hash of it, so a reader proves membership with one hash and no manifest exists to list it; members is not read then. Any other store's root is over members, the member commitments in manifest order (Manifest.Leaves), and never over the manifest, which is the head.

It is one function because the publisher that writes a root and the reader that recomputes it must agree on it exactly; two copies of the rule are two chances for a store to be written under one and read under the other. Whether members holds Count commitments is the caller's to check, since the caller is the one that knows what a mismatch means for it.

Example

A store of several members: the manifest lists the members' commitments, the root is over those commitments in manifest order, and the head a reader asks a host for is the manifest's own commitment. The refs entry is what the parent record carries.

package main

import (
	"crypto/sha256"
	"fmt"

	"github.com/lightwebinc/bcommon/cbor"
	"github.com/lightwebinc/bcommon/commit"
	"github.com/lightwebinc/bcommon/store"
)

func main() {
	m := &store.Manifest{Members: []store.Member{
		{C: sha256.Sum256([]byte("part one")), Name: "1", Size: 8, Type: "text/plain"},
		{C: sha256.Sum256([]byte("part two")), Name: "2", Size: 8, Type: "text/plain"},
		{C: sha256.Sum256([]byte("part three")), Name: "3", Size: 10, Type: "text/plain"},
	}}
	// The bound is the application's: the body bound of the record that
	// will carry the manifest.
	body, err := m.Body(64 << 10)
	if err != nil {
		fmt.Println(err)
		return
	}
	enc, err := cbor.Encode(body)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("manifest body bytes:", len(enc))

	// In an application the head is the commitment (carrier txid) of the
	// record that carries the manifest body; a stand-in is used here.
	head := sha256.Sum256(enc)
	ref := store.Ref{Name: "docs", Count: uint64(len(m.Members)), Head: &head}
	ref.Root = store.Root(ref, m.Leaves())
	fmt.Println("root is over the members:", ref.Root == commit.Root(m.Leaves()))

	refs, err := store.EncodeRefs([]store.Ref{ref})
	if err != nil {
		fmt.Println(err)
		return
	}
	back, err := store.DecodeRefs(refs)
	if err != nil {
		fmt.Println(err)
		return
	}
	h, err := store.Head(back[0])
	fmt.Println("head round-trips:", h == head, err)
}
Output:
manifest body bytes: 208
root is over the members: true
head round-trips: true <nil>
Example (OneMember)

A store of one member is its own head: its root is the leaf hash of that member and no manifest exists.

package main

import (
	"crypto/sha256"
	"fmt"

	"github.com/lightwebinc/bcommon/commit"
	"github.com/lightwebinc/bcommon/store"
)

func main() {
	member := sha256.Sum256([]byte("the only part"))
	ref := store.Ref{Name: "note", Count: 1, Head: &member}
	ref.Root = store.Root(ref, nil)
	fmt.Println(ref.Root == commit.LeafHash(member))
}
Output:
true

Types

type Manifest

type Manifest struct {
	Members []Member
}

Manifest is a store's member list.

func ParseManifest

func ParseManifest(body cbor.Map) (*Manifest, error)

ParseManifest reads a manifest out of a record body. It is deliberately strict: a body with anything else in it is not a manifest, because a manifest's body is the whole of what that record is for.

func (*Manifest) Body

func (m *Manifest) Body(bound int) (cbor.Map, error)

Body encodes the manifest as a record body, refusing one whose encoding is over bound, the body bound of the record that will carry it. The bound is the application's, because it follows what that application's records may carry. The count bound alone is not enough: a member costs more than bound/MaxMembers once its name and type are counted, so a caller that checked only the count would mint every member and then fail on the record that was supposed to name them.

func (*Manifest) Leaves

func (m *Manifest) Leaves() [][32]byte

Leaves is the member commitments in order, which is what the store's root is computed over (see Root).

type Member

type Member struct {
	// C is the member sub-record's commitment, in hash byte order, the
	// order every other commitment in a record is carried in.
	C [32]byte
	// Name labels the part. Empty is allowed and ordinary: a chunked
	// document's parts are identified by their position.
	Name string
	// Size is the member's content length in bytes, so a reader can size a
	// buffer and refuse a store larger than it wants before fetching it.
	Size uint64
	// Type is the member's media type: "text/plain" for a document part, a
	// locator type for a member that names content held elsewhere.
	Type string
}

Member is one entry of a manifest.

type Ref

type Ref struct {
	Name  string
	Root  [32]byte
	Count uint64
	Head  *[32]byte
	// Unknown carries every member of this entry that this version does not
	// define, verbatim, so a re-encode is faithful and an older reader is
	// not orphaned by a newer store.
	//
	// It is preserved and NOT ignored. A member a reader does not know may
	// change how the store's membership is computed, so a reader that
	// silently skipped it could accept a membership proof that is wrong.
	// The rule is therefore: an entry carrying an unknown member makes THAT
	// STORE unreadable to this reader, and changes nothing else about the
	// record. Refusing the whole record instead, which is what a strict
	// entry width does, means one new store field costs every existing
	// reader the rest of the record as well.
	Unknown cbor.Map
}

Ref names a sub-store and commits to it: an RFC 6962 root over the commitments of that store's sub-records, and how many there are.

Head, when present, is the commitment of the store's head member, in hash byte order like every other commitment in the record. It is what lets a reader find the store at all: a root cannot be inverted and the host holds no root-to-member index. A one-member store's root is exactly LeafHash of its member, so with Count 1 the reader proves membership by hashing Head once. Absent, the store is committed to but not publicly linked: whoever is meant to read it is handed a member some other way.

func DecodeRefs

func DecodeRefs(v cbor.Value) ([]Ref, error)

DecodeRefs reads a record's refs value, checking every entry's shape and preserving the members this version does not define.

func (*Ref) Extended

func (r *Ref) Extended() bool

Extended reports whether this entry carries a member this version does not define, which makes the store unreadable here.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL