mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 22:01:05 +10:00
Twelve packages lifted from seedhammer/seedhammer @ v1.3.0
(commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e):
address/ — bitcoin address parsing
backup/ — v1 plate dimensions + UR-coded multi-plate backup
bc/ — Blockchain Commons: ur, fountain, bytewords, urtypes,
xoshiro256 (5 subpkgs)
bip32/ — HD-key derivation
bip39/ — mnemonic seed phrases + 2048-word wordlist
engrave/ — text/QR → MoveTo/LineTo command stream conversion
seedqr/ — SeedQR / CompactSeedQR encoders
image/ — paletted, rgb565, alpha4, ninepatch image formats
nonstandard/ — bitcoin descriptor + script parsing
font/ — bitmap + vector font runtime
font/{comfortaa,poppins,constant,bitmap,vector}/ — actual fonts
driver/mjolnir/ — MarkingWay USB-serial engraver driver
Plus an earlier-aside backup_test.go restored (its deps are now lifted).
Import paths globally rewritten seedhammer.com → mineracks namespace
via single sed pass; verified no orphan refs remain. go.mod adopts
upstream's full dep set plus the replace-directive for the patched
kortschak/qr fork.
go build ./... clean (all 27 packages)
go test ./... clean (12 packages with tests, all passing)
NOT lifted in this commit:
- driver/{wshat,drm,libcamera} (hardware-specific GPIO/LCD/camera —
will be platform-v1/-shaped abstractions instead)
- gui/ (depends on the above; lifts in Phase 2)
- cmd/{controller,...} (Pi binary entrypoints — not needed for the
companion repo)
- zbar/ (QR scanner — needs libcamera)
Next:
- Write the SH1E reference encoder/decoder in engrave/wire/sh1e/
- Lift Gangleri42's cmd/webnfc/ shell + retune to v1 plates
- First buildable composer WASM with a working preview
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
339 lines
6.8 KiB
Go
339 lines
6.8 KiB
Go
// Package fountain implements the fountain encoding used by
|
|
// the Uniform Resources (UR) format described in [BCR-2020-005].
|
|
//
|
|
// [BCR-2020-005]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
|
package fountain
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/mineracks/seedhammer-v1-companion/bc/xoshiro256"
|
|
)
|
|
|
|
type Decoder struct {
|
|
header partHeader
|
|
queue []*part
|
|
mixed map[string]*part
|
|
completed map[int]*part
|
|
}
|
|
|
|
func Encode(message []byte, seqNum, seqLen int) []byte {
|
|
if seqLen == 1 {
|
|
return message
|
|
}
|
|
n := (len(message) + seqLen - 1) / seqLen
|
|
payload := make([]byte, n)
|
|
checksum := Checksum(message)
|
|
sn32 := uint32(seqNum)
|
|
if int(sn32) != seqNum {
|
|
panic("seqNum out of range")
|
|
}
|
|
fragments := chooseFragments(sn32, seqLen, checksum)
|
|
for _, idx := range fragments {
|
|
start := idx * n
|
|
if start > len(message) {
|
|
continue
|
|
}
|
|
frag := message[start:]
|
|
if len(frag) > len(payload) {
|
|
frag = frag[:len(payload)]
|
|
}
|
|
for i, b := range frag {
|
|
payload[i] = payload[i] ^ b
|
|
}
|
|
}
|
|
p := part{
|
|
SeqNum: sn32,
|
|
partHeader: partHeader{
|
|
SeqLen: seqLen,
|
|
MessageLen: len(message),
|
|
Checksum: checksum,
|
|
},
|
|
Data: payload,
|
|
}
|
|
enc, err := cbor.CoreDetEncOptions().EncMode()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
b, err := enc.Marshal(p)
|
|
if err != nil {
|
|
// Valid by construction.
|
|
panic(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
type part struct {
|
|
_ struct{} `cbor:",toarray"`
|
|
SeqNum uint32
|
|
partHeader
|
|
Data []byte
|
|
|
|
fragments []int
|
|
}
|
|
|
|
type partHeader struct {
|
|
SeqLen int
|
|
MessageLen int
|
|
Checksum uint32
|
|
}
|
|
|
|
func (d *Decoder) Progress() float32 {
|
|
estimated := float32(d.header.SeqLen) * 1.75
|
|
p := float32(len(d.completed)+len(d.mixed)) / estimated
|
|
if p > 1 {
|
|
p = 1
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (d *Decoder) Add(data []byte) error {
|
|
mode, err := cbor.DecOptions{
|
|
ExtraReturnErrors: cbor.ExtraDecErrorUnknownField,
|
|
}.DecMode()
|
|
if err != nil {
|
|
return fmt.Errorf("fountain: failed to initialize decoder: %w", err)
|
|
}
|
|
|
|
p := new(part)
|
|
if err := mode.Unmarshal(data, p); err != nil {
|
|
return fmt.Errorf("fountain: failed to decode fragment: %w", err)
|
|
}
|
|
if d.header.SeqLen > 0 {
|
|
if d.header != p.partHeader {
|
|
return fmt.Errorf("fountain: incompatible fragment")
|
|
}
|
|
} else {
|
|
d.header = p.partHeader
|
|
}
|
|
p.fragments = chooseFragments(p.SeqNum, p.SeqLen, p.Checksum)
|
|
d.queue = append(d.queue, p)
|
|
|
|
for len(d.queue) > 0 {
|
|
p := d.queue[len(d.queue)-1]
|
|
d.queue = d.queue[:len(d.queue)-1]
|
|
if len(p.fragments) == 1 {
|
|
if d.completed == nil {
|
|
d.completed = make(map[int]*part)
|
|
}
|
|
d.completed[p.fragments[0]] = p
|
|
d.reduceMixed(p)
|
|
} else {
|
|
if d.mixed == nil {
|
|
d.mixed = make(map[string]*part)
|
|
}
|
|
for _, other := range d.completed {
|
|
reducePart(p, other)
|
|
}
|
|
for _, other := range d.mixed {
|
|
reducePart(p, other)
|
|
}
|
|
if len(p.fragments) == 1 {
|
|
d.queue = append(d.queue, p)
|
|
} else {
|
|
d.reduceMixed(p)
|
|
d.mixed[mixedKey(p.fragments)] = p
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func reducePart(a, b *part) {
|
|
// return if b is not a strict subset of a.
|
|
if len(b.fragments) >= len(a.fragments) {
|
|
return
|
|
}
|
|
m := make(map[int]bool)
|
|
for _, f := range a.fragments {
|
|
m[f] = true
|
|
}
|
|
for _, f := range b.fragments {
|
|
if !m[f] {
|
|
return
|
|
}
|
|
delete(m, f)
|
|
}
|
|
|
|
// Subtract b from a.
|
|
a.fragments = nil
|
|
for f := range m {
|
|
a.fragments = append(a.fragments, f)
|
|
}
|
|
for i := range a.Data {
|
|
a.Data[i] ^= b.Data[i]
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) reduceMixed(p *part) {
|
|
for k, other := range d.mixed {
|
|
delete(d.mixed, k)
|
|
reducePart(other, p)
|
|
if len(other.fragments) == 1 {
|
|
d.queue = append(d.queue, other)
|
|
} else {
|
|
d.mixed[mixedKey(other.fragments)] = other
|
|
}
|
|
}
|
|
}
|
|
|
|
func mixedKey(ids []int) string {
|
|
sort.Ints(ids)
|
|
strs := make([]string, len(ids))
|
|
for i, id := range ids {
|
|
strs[i] = strconv.Itoa(id)
|
|
}
|
|
return strings.Join(strs, "|")
|
|
}
|
|
|
|
func (d *Decoder) Result() ([]byte, error) {
|
|
if len(d.completed) != d.header.SeqLen {
|
|
return nil, nil
|
|
}
|
|
var sorted []*part
|
|
for _, p := range d.completed {
|
|
sorted = append(sorted, p)
|
|
}
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
return sorted[i].fragments[0] < sorted[j].fragments[0]
|
|
})
|
|
var msg []byte
|
|
for _, p := range sorted {
|
|
msg = append(msg, p.Data...)
|
|
}
|
|
if len(msg) < d.header.MessageLen {
|
|
return nil, fmt.Errorf("fountain: message too short")
|
|
}
|
|
msg = msg[:d.header.MessageLen]
|
|
check := Checksum(msg)
|
|
if check != d.header.Checksum {
|
|
return nil, fmt.Errorf("fountain: mismatched checksum or message too short")
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
func Checksum(data []byte) uint32 {
|
|
return crc32.ChecksumIEEE(data)
|
|
}
|
|
|
|
// SeqNumFor searches for a seqNum that outpus the xor of fragments.
|
|
func SeqNumFor(seqLen int, checksum uint32, fragments []int) int {
|
|
seqNum := 1
|
|
sort.IntSlice(fragments).Sort()
|
|
for {
|
|
got := chooseFragments(uint32(seqNum), seqLen, checksum)
|
|
sort.IntSlice(got).Sort()
|
|
if reflect.DeepEqual(got, fragments) {
|
|
return seqNum
|
|
}
|
|
seqNum++
|
|
}
|
|
}
|
|
|
|
func chooseFragments(seqNum uint32, seqLen int, checksum uint32) []int {
|
|
if seqNum <= uint32(seqLen) {
|
|
return []int{int(seqNum - 1)}
|
|
} else {
|
|
seed := binary.BigEndian.AppendUint32(nil, seqNum)
|
|
seed = binary.BigEndian.AppendUint32(seed, checksum)
|
|
h := sha256.Sum256(seed)
|
|
rng := new(xoshiro256.Source)
|
|
rng.Seed(h)
|
|
degree := chooseDegree(seqLen, rng)
|
|
indexes := make([]int, seqLen)
|
|
for i := range indexes {
|
|
indexes[i] = i
|
|
}
|
|
shuffled := shuffle(indexes, rng)
|
|
return shuffled[:degree]
|
|
}
|
|
}
|
|
|
|
func shuffle(items []int, rng *xoshiro256.Source) []int {
|
|
var result []int
|
|
for len(items) > 0 {
|
|
idx := rng.Intn(len(items))
|
|
it := items[idx]
|
|
items = append(items[:idx], items[idx+1:]...)
|
|
result = append(result, it)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func chooseDegree(seqLen int, rng *xoshiro256.Source) int {
|
|
probs := make([]float64, seqLen)
|
|
for i := range probs {
|
|
probs[i] = 1. / float64(i+1)
|
|
}
|
|
return sample(probs, rng.Float64) + 1
|
|
}
|
|
|
|
func sample(probs []float64, rng func() float64) int {
|
|
var sum float64
|
|
for _, p := range probs {
|
|
sum += p
|
|
}
|
|
|
|
n := len(probs)
|
|
P := make([]float64, n)
|
|
for i, p := range probs {
|
|
P[i] = p * float64(n) / sum
|
|
}
|
|
|
|
var S, L []int
|
|
|
|
for i := n - 1; i >= 0; i-- {
|
|
if P[i] < 1 {
|
|
S = append(S, i)
|
|
} else {
|
|
L = append(L, i)
|
|
}
|
|
}
|
|
|
|
probs = make([]float64, n)
|
|
aliases := make([]int, n)
|
|
for len(S) > 0 && len(L) > 0 {
|
|
a := S[len(S)-1]
|
|
S = S[:len(S)-1]
|
|
g := L[len(L)-1]
|
|
L = L[:len(L)-1]
|
|
probs[a] = P[a]
|
|
aliases[a] = g
|
|
P[g] += P[a] - 1
|
|
if P[g] < 1 {
|
|
S = append(S, g)
|
|
} else {
|
|
L = append(L, g)
|
|
}
|
|
}
|
|
|
|
for len(L) > 0 {
|
|
g := L[len(L)-1]
|
|
L = L[:len(L)-1]
|
|
probs[g] = 1
|
|
}
|
|
|
|
for len(S) > 0 {
|
|
a := S[len(S)-1]
|
|
S = S[:len(S)-1]
|
|
probs[a] = 1
|
|
}
|
|
|
|
r1 := rng()
|
|
r2 := rng()
|
|
i := int(float64(n) * r1)
|
|
if r2 < probs[i] {
|
|
return i
|
|
} else {
|
|
return aliases[i]
|
|
}
|
|
}
|