Release object, release in hand and step 9.c option B (spec v0.15 draft)
The release of a round becomes a file, .dkr: a release object in
deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round,
4: signature}, which provider.EncodeRelease writes and DecodeRelease reads
with its layers (size, type and version, schema). provider.ParseRelease
also reads drand's JSON as the input of the caller. Verify checks the chain
hash a release names before its round and its signature, with
ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the
informative format of the draft.
capsule.OpenOptions.Release takes a release in hand, a provider.Supplier,
exclusive with Source: Open does not compare it with the clock (step 9.c,
option B) and reports a clock behind it in Opened.ClockBehind; a network
source is still never asked before the round time. The CLI gains
decrypt -release FILE (.dkr, drand's JSON or a local archive),
decrypt -save-release FILE.dkr and the command release, which fetches,
verifies and saves the .dkr without opening the capsule.
Test data: vectors/release.json, releases/<round>.dkr for rounds 1000,
1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In
mutations.json every case says its source, "supplied" or "network"; the
case "round not reached yet", a release in hand, now opens, and four cases
are added: the same with a network source, a release of another round from
a network source, and two release objects of another chain. SpecVersion
stays 0.14 until the author approves the draft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
package capsule_test
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"time"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
// supplierFunc adapts a function to provider.Supplier.
type supplierFunc func ( * profile . Profile , provider . Condition ) ( [ ] byte , error )
func ( f supplierFunc ) Supply ( p * profile . Profile , c provider . Condition ) ( [ ] byte , error ) {
return f ( p , c )
}
func readRelease ( t * testing . T , round uint64 ) [ ] byte {
t . Helper ( )
b , err := os . ReadFile ( filepath . Join ( "../testdata/releases" , testkit . ReleaseFileName ( round ) ) )
if err != nil {
t . Fatal ( err )
}
return b
}
// Spec v0.15, §63 step 9.c: a release in hand is not compared with the
// clock. The capsule opens with a clock before the round time, Opened says
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 hours ago
// the clock is behind, and Opened.Release encodes to the official release
// object of the round.
Release object, release in hand and step 9.c option B (spec v0.15 draft)
The release of a round becomes a file, .dkr: a release object in
deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round,
4: signature}, which provider.EncodeRelease writes and DecodeRelease reads
with its layers (size, type and version, schema). provider.ParseRelease
also reads drand's JSON as the input of the caller. Verify checks the chain
hash a release names before its round and its signature, with
ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the
informative format of the draft.
capsule.OpenOptions.Release takes a release in hand, a provider.Supplier,
exclusive with Source: Open does not compare it with the clock (step 9.c,
option B) and reports a clock behind it in Opened.ClockBehind; a network
source is still never asked before the round time. The CLI gains
decrypt -release FILE (.dkr, drand's JSON or a local archive),
decrypt -save-release FILE.dkr and the command release, which fetches,
verifies and saves the .dkr without opening the capsule.
Test data: vectors/release.json, releases/<round>.dkr for rounds 1000,
1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In
mutations.json every case says its source, "supplied" or "network"; the
case "round not reached yet", a release in hand, now opens, and four cases
are added: the same with a network source, a release of another round from
a network source, and two release objects of another chain. SpecVersion
stays 0.14 until the author approves the draft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
func TestReleaseInHand ( t * testing . T ) {
f := loadFixture ( t , "format3_time_and_key_portable" )
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 hours ago
obj := readRelease ( t , 1000 )
Release object, release in hand and step 9.c option B (spec v0.15 draft)
The release of a round becomes a file, .dkr: a release object in
deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round,
4: signature}, which provider.EncodeRelease writes and DecodeRelease reads
with its layers (size, type and version, schema). provider.ParseRelease
also reads drand's JSON as the input of the caller. Verify checks the chain
hash a release names before its round and its signature, with
ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the
informative format of the draft.
capsule.OpenOptions.Release takes a release in hand, a provider.Supplier,
exclusive with Source: Open does not compare it with the clock (step 9.c,
option B) and reports a clock behind it in Opened.ClockBehind; a network
source is still never asked before the round time. The CLI gains
decrypt -release FILE (.dkr, drand's JSON or a local archive),
decrypt -save-release FILE.dkr and the command release, which fetches,
verifies and saves the .dkr without opening the capsule.
Test data: vectors/release.json, releases/<round>.dkr for rounds 1000,
1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In
mutations.json every case says its source, "supplied" or "network"; the
case "round not reached yet", a release in hand, now opens, and four cases
are added: the same with a network source, a release of another round from
a network source, and two release objects of another chain. SpecVersion
stays 0.14 until the author approves the draft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
for _ , tc := range [ ] struct {
name string
now time . Time
behind bool
} {
{ "after the round time" , f . unlock ( t ) , false } ,
{ "a clock one nanosecond behind" , f . unlock ( t ) . Add ( - 1 ) , true } ,
{ "a clock years behind" , testkit . Genesis ( ) , true } ,
} {
o := f . openOptions ( t )
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 hours ago
o . Source , o . Release , o . Now = nil , provider . Encoded ( obj ) , testkit . Fixed ( tc . now )
Release object, release in hand and step 9.c option B (spec v0.15 draft)
The release of a round becomes a file, .dkr: a release object in
deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round,
4: signature}, which provider.EncodeRelease writes and DecodeRelease reads
with its layers (size, type and version, schema). provider.ParseRelease
also reads drand's JSON as the input of the caller. Verify checks the chain
hash a release names before its round and its signature, with
ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the
informative format of the draft.
capsule.OpenOptions.Release takes a release in hand, a provider.Supplier,
exclusive with Source: Open does not compare it with the clock (step 9.c,
option B) and reports a clock behind it in Opened.ClockBehind; a network
source is still never asked before the round time. The CLI gains
decrypt -release FILE (.dkr, drand's JSON or a local archive),
decrypt -save-release FILE.dkr and the command release, which fetches,
verifies and saves the .dkr without opening the capsule.
Test data: vectors/release.json, releases/<round>.dkr for rounds 1000,
1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In
mutations.json every case says its source, "supplied" or "network"; the
case "round not reached yet", a release in hand, now opens, and four cases
are added: the same with a network source, a release of another round from
a network source, and two release objects of another chain. SpecVersion
stays 0.14 until the author approves the draft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
out , err := capsule . Open ( context . Background ( ) , nil , bytes . NewReader ( f . dkc ) , o )
if err != nil {
t . Fatalf ( "%s: %v" , tc . name , err )
}
if out . ClockBehind != tc . behind {
t . Fatalf ( "%s: ClockBehind %v" , tc . name , out . ClockBehind )
}
saved , err := provider . EncodeRelease ( out . Release )
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 hours ago
if err != nil || ! bytes . Equal ( saved , obj ) {
t . Fatalf ( "%s: the release encodes to %x, %v" , tc . name , saved , err )
Release object, release in hand and step 9.c option B (spec v0.15 draft)
The release of a round becomes a file, .dkr: a release object in
deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round,
4: signature}, which provider.EncodeRelease writes and DecodeRelease reads
with its layers (size, type and version, schema). provider.ParseRelease
also reads drand's JSON as the input of the caller. Verify checks the chain
hash a release names before its round and its signature, with
ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the
informative format of the draft.
capsule.OpenOptions.Release takes a release in hand, a provider.Supplier,
exclusive with Source: Open does not compare it with the clock (step 9.c,
option B) and reports a clock behind it in Opened.ClockBehind; a network
source is still never asked before the round time. The CLI gains
decrypt -release FILE (.dkr, drand's JSON or a local archive),
decrypt -save-release FILE.dkr and the command release, which fetches,
verifies and saves the .dkr without opening the capsule.
Test data: vectors/release.json, releases/<round>.dkr for rounds 1000,
1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In
mutations.json every case says its source, "supplied" or "network"; the
case "round not reached yet", a release in hand, now opens, and four cases
are added: the same with a network source, a release of another round from
a network source, and two release objects of another chain. SpecVersion
stays 0.14 until the author approves the draft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
}
}
}
// A release in hand keeps the order of step 9: the credentials first (9.a,
// 9.b), with no request; then the release, whose failure to be supplied is
// ErrReleaseUnavailable alone at step 9; then step 10 with its codes, the
// layers of the object first.
func TestReleaseInHandErrors ( t * testing . T ) {
f := loadFixture ( t , "format3_time_and_key_portable" )
other := profile . Quicknet ( ) . ChainHash
other [ 5 ] ^ = 1
otherChain , err := provider . EncodeRelease ( provider . Release { Round : 1001 , Signature : testkit . Release ( 1001 ) . Signature , ChainHash : other [ : ] } )
if err != nil {
t . Fatal ( err )
}
for _ , tc := range [ ] struct {
name string
supply func ( ) ( [ ] byte , error )
noAccess bool
want * datekeys . Error
step int
calls int
} {
{ "no credential, before any supply" , func ( ) ( [ ] byte , error ) { return readRelease ( t , 1000 ) , nil } , true , datekeys . ErrAccessRequired , 9 , 0 } ,
{ "nothing supplied" , func ( ) ( [ ] byte , error ) { return nil , fmt . Errorf ( "none: %w" , datekeys . ErrReleaseUnavailable ) } , false , datekeys . ErrReleaseUnavailable , 9 , 1 } ,
{ "a supplier failing with another code" , func ( ) ( [ ] byte , error ) { return nil , fmt . Errorf ( "odd: %w" , datekeys . ErrIntegrity ) } , false , datekeys . ErrReleaseUnavailable , 9 , 1 } ,
{ "an empty object" , func ( ) ( [ ] byte , error ) { return nil , nil } , false , datekeys . ErrNonCanonicalCBOR , 10 , 1 } ,
{ "another chain and another round" , func ( ) ( [ ] byte , error ) { return otherChain , nil } , false , datekeys . ErrProfileMismatch , 10 , 1 } ,
{ "another round" , func ( ) ( [ ] byte , error ) { return readRelease ( t , 1001 ) , nil } , false , datekeys . ErrRoundMismatch , 10 , 1 } ,
{ "drand's JSON of another round" , func ( ) ( [ ] byte , error ) {
return [ ] byte ( ` { "round":1001,"signature":"00"} ` ) , nil
} , false , datekeys . ErrRoundMismatch , 10 , 1 } ,
} {
calls := 0
o := f . openOptions ( t )
o . Source , o . Now = nil , testkit . Fixed ( testkit . Genesis ( ) )
o . Release = supplierFunc ( func ( * profile . Profile , provider . Condition ) ( [ ] byte , error ) { calls ++ ; return tc . supply ( ) } )
if tc . noAccess {
o . AccessKey = nil
}
step , err := openAt ( t , f . dkc , o )
if ! onlyCode ( err , tc . want ) || step != tc . step || calls != tc . calls {
t . Errorf ( "%s: got %v at step %d after %d supplies, want %v at step %d after %d" , tc . name , err , step , calls , tc . want , tc . step , tc . calls )
}
}
o := f . openOptions ( t )
o . Release = provider . Encoded ( readRelease ( t , 1000 ) )
if _ , err := capsule . Open ( context . Background ( ) , nil , bytes . NewReader ( f . dkc ) , o ) ; err == nil || datekeys . Code ( err ) != "" {
t . Fatalf ( "Source and Release together: %v" , err )
}
o . Source , o . Release = nil , nil
if _ , err := capsule . Open ( context . Background ( ) , nil , bytes . NewReader ( f . dkc ) , o ) ; err == nil || errors . Is ( err , datekeys . ErrReleaseUnavailable ) {
t . Fatalf ( "no source: %v" , err )
}
}