Lift composer substrate from upstream v1.3.0

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>
This commit is contained in:
mineracks 2026-05-28 18:36:40 +10:00
parent d62384658d
commit 9261cf368a
109 changed files with 10361 additions and 29 deletions

157
address/address.go Normal file
View File

@ -0,0 +1,157 @@
// package address derives recieve and change addresses from
// output descriptors.
package address
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"sort"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/mineracks/seedhammer-v1-companion/bc/urtypes"
)
func Change(desc urtypes.OutputDescriptor, index uint32) (string, error) {
return address(desc, index, true)
}
func Receive(desc urtypes.OutputDescriptor, index uint32) (string, error) {
return address(desc, index, false)
}
func Supported(desc urtypes.OutputDescriptor) bool {
_, err := Receive(desc, 0)
return !errors.Is(err, errUnsupported)
}
var errUnsupported = errors.New("unsupported descriptor")
func address(desc urtypes.OutputDescriptor, index uint32, change bool) (string, error) {
var addr btcutil.Address
var network *chaincfg.Params
switch desc.Type {
case urtypes.SortedMulti:
var keys []*btcutil.AddressPubKey
for _, k := range desc.Keys {
pub, err := derivePubKey(k, index, change)
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
if network != nil && k.Network != network {
return "", fmt.Errorf("address: multisig descriptor mixes networks: %w", errUnsupported)
}
network = k.Network
addrPub, err := btcutil.NewAddressPubKey(pub.SerializeCompressed(), network)
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
keys = append(keys, addrPub)
}
sort.Slice(keys, func(i, j int) bool {
return bytes.Compare(keys[i].PubKey().SerializeCompressed(), keys[j].PubKey().SerializeCompressed()) == -1
})
script, err := txscript.MultiSigScript(keys, desc.Threshold)
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
switch desc.Script {
case urtypes.P2SH:
addr, err = btcutil.NewAddressScriptHash(script, network)
case urtypes.P2WSH, urtypes.P2SH_P2WSH:
hash := sha256.Sum256(script)
addr, err = btcutil.NewAddressWitnessScriptHash(hash[:], network)
default:
return "", fmt.Errorf("address: multisig script: %s: %w", desc.Script, errUnsupported)
}
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
case urtypes.Singlesig:
k := desc.Keys[0]
network = k.Network
pub, err := derivePubKey(k, index, change)
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
switch desc.Script {
case urtypes.P2PKH:
pkHash := btcutil.Hash160(pub.SerializeCompressed())
addr, err = btcutil.NewAddressPubKeyHash(pkHash, network)
case urtypes.P2WPKH, urtypes.P2SH_P2WPKH:
pkHash := btcutil.Hash160(pub.SerializeCompressed())
addr, err = btcutil.NewAddressWitnessPubKeyHash(pkHash, network)
case urtypes.P2TR:
tkey := txscript.ComputeTaprootKeyNoScript(pub)
addr, err = btcutil.NewAddressTaproot(schnorr.SerializePubKey(tkey), network)
default:
return "", fmt.Errorf("address: singlesig script: %s: %w", desc.Script, errUnsupported)
}
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
default:
return "", fmt.Errorf("address: descriptor: %w", errUnsupported)
}
// Derive wrapped address types.
switch desc.Script {
case urtypes.P2SH_P2WPKH, urtypes.P2SH_P2WSH:
script, err := txscript.PayToAddrScript(addr)
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
addr, err = btcutil.NewAddressScriptHash(script, network)
if err != nil {
return "", fmt.Errorf("address: %w", err)
}
}
return addr.String(), nil
}
func derivePubKey(k urtypes.KeyDescriptor, index uint32, change bool) (*secp256k1.PublicKey, error) {
children := k.Children
if len(children) == 0 {
// Default to <0;1>/*.
children = append(children,
urtypes.Derivation{
Type: urtypes.RangeDerivation,
Index: 0,
End: 1,
},
urtypes.Derivation{
Type: urtypes.WildcardDerivation,
},
)
}
xpub := k.ExtendedKey()
for _, c := range children {
var id uint32
switch c.Type {
case urtypes.ChildDerivation:
id = c.Index
case urtypes.RangeDerivation:
if c.End != c.Index+1 {
return nil, errors.New("unsupported range path element")
}
id = c.Index
if change {
id = c.End
}
case urtypes.WildcardDerivation:
id = index
default:
return nil, errors.New("unsupported path element")
}
child, err := xpub.Derive(id)
if err != nil {
return nil, err
}
xpub = child
}
return xpub.ECPubKey()
}

85
address/address_test.go Normal file
View File

@ -0,0 +1,85 @@
package address
import (
"testing"
"github.com/mineracks/seedhammer-v1-companion/nonstandard"
)
func TestAddresses(t *testing.T) {
xpubs := []string{
"xpub6DiYrfRwNnjeX4vHsWMajJVFKrbEEnu8gAW9vDuQzgTWEsEHE16sGWeXXUV1LBWQE1yCTmeprSNcqZ3W74hqVdgDbtYHUv3eM4W2TEUhpan",
"xpub6DjrnfAyuonMaboEb3ZQZzhQ2ZEgaKV2r64BFmqymZqJqviLTe1JzMr2X2RfQF892RH7MyYUbcy77R7pPu1P71xoj8cDUMNhAMGYzKR4noZ",
"xpub6DnT4E1fT8VxuAZW29avMjr5i99aYTHBp9d7fiLnpL5t4JEprQqPMbTw7k7rh5tZZ2F5g8PJpssqrZoebzBChaiJrmEvWwUTEMAbHsY39Ge",
}
tests := []struct {
desc string
receives []string
changes []string
}{
{
"pkh(" + xpubs[0] + ")",
[]string{"1M88vKcJFc4KPAe5RHXsuJqWcg3muStyK4", "1DyJom6LUg98zbcff7Y3vnh6kYpERcMys3", "1HPR4dJ2W4i9Q4FnkyYGs41d1CczxQuwiA"},
[]string{"12fk5WJ9AtzQzRWFtCabn8Wh45zmjmcpFR", "1A6QmCc5cqhtyzmgMmEKFWc7eP8mvyUcFJ", "18vo9Lf4vaGUzgji8bGQ1LQ5zxU5yh2DDB"},
},
{
"wpkh(" + xpubs[0] + ")",
[]string{"bc1qmj7qns4exnh8p6a9xndvz34msj72arnxl3sapx", "bc1q3er64jwge5sfezr6ymkt6d9l79zcvs8z20n5xz", "bc1qkwl5qpx6k93cqmnygn6kgucgka8q3z4kur2nm8"},
[]string{"bc1qzf97gj5h2ryu2f8lpx8940dkn4vk8g6xx3gwlg", "bc1qvwlscfgdmtkna074wylrvqly4w6nlpklsmyx7x", "bc1q2m6hyqsnxwqp6f0mlcp6yh896rsmqw3ugj26hr"},
},
{
"sh(wpkh(" + xpubs[0] + "))",
[]string{"354hXbgwGRqHXywh9ZESRXWW4zxrpeScXQ", "37cG1ZYNKcQYikRkdmJKKKfXxiVbk6ywiJ", "3KwWvmB9DsRJLGt11ozWLPsdbw5GfbAqjb"},
[]string{"35c95EWSNQJCyh7uNVZ4rp2hf41GUsgdLn", "3GWo6g2n5iBwtadHgJqYyL1UMEvAwSTUg7", "3Ho1jfnTtDaW5isJfgjMY3v3rQMwDDyVQt"},
},
{
"tr(" + xpubs[0] + ")",
[]string{"bc1ppeya86zv0hnpzrvh7czgqxkn5zjxxymxd6nqplhhx7fejxvhk0ysp7zekg", "bc1pqhh2d3sdktkfvneee95mlv99t0cddcy3vpk5fglz78jm3e55zydqj5wycf", "bc1px4k4y20vusff4v0xvpgwslda2s2fuajmn8eypt28ae4r73jlut7s8y5tq6"},
[]string{"bc1px5xqncrjm3823nervn3epj2al0adt79aaa56jvxpvzy29stvjn2q2jruge", "bc1p5u5rrr4lczraxkq3xwdjxh98fkl4sjuswwxgwj2uw3rdfwjp8uusp2ymfr", "bc1pvhsgwwmthv864r4kt2g65323jau4ge0y4k4qufqjzvfzsk4d60fq7pe6xx"},
},
{
"wsh(sortedmulti(1," + xpubs[0] + "))",
[]string{"bc1qm78sug9d6g4jwlk9qulgtcp9ghepn2xjfz8xdhpa8g3q3hzcl8nsfez8at", "bc1q6uk7f77v7lspm803kjgvfpmreumdnjgaksfq3mvuhzc0zwvcy83qedrjvj", "bc1qntv6z9lyzxedfp63qgr7pm2gk9uzfjjzhhzm5j8599u6m89h2q6q3fzhu6"},
[]string{"bc1qe3x073dtr0vy8xd342ctnsdzfz5ule53ul933jutx5yesxj3032qzmp8pj", "bc1q4yx84f5t2zgk24dcn87azhhvuxwr2psduhy4pl8vzrjv28zvazfs82u368", "bc1qxx0tjkg3qce48nvjyrnqssc9evqh25guursx7uk7uvkx6njj92vs40pp2u"},
},
{
"wsh(sortedmulti(1," + xpubs[0] + "/1234/<5;6>/*))",
[]string{"bc1qt77623mmw4lnsewlmt9cs60yvxpwks540ygtzkakdf8xaa4ahsvqcma0k0", "bc1qz6qz4m3uj40cpqt6s4nmg9jew66qzmthrun95mxy5662u3ldwdaqj8edge"},
[]string{"bc1qc8gz4sw524pje9lwz5ujjrxvha774e8w6a2xul4jt8eed7h7hcvsc6cm4y", "bc1qwh9lhlgx9an4kz3s9qtrfm3xyvms84lkjy4paflg408vswjq4zcqx2xzlp"},
},
{
"sh(wsh(sortedmulti(2," + xpubs[0] + "," + xpubs[1] + "," + xpubs[2] + ")))",
[]string{"3EECinK7zYPwa4bR53mbGiuLrbU2V9waHg", "34EqecNrmzM2v2Qx7MvaU49FEsdpxjsRw3"},
[]string{"3L7AnrmQiSuAPNTX73d8zdfu5o2hUe3V6C", "3Hp5QsDqFGpDoYfiBV1uPctKE5MYaxfqNK"},
},
{
"sh(sortedmulti(2," + xpubs[0] + "," + xpubs[1] + "," + xpubs[2] + "))",
[]string{"3DwWNBMDdsP5Tf9wYyGT7qMkCEe5mTC3U3", "334QzbkBDRWfBWuE8Qhj5dXigYZpt7tpcT"},
[]string{"39DByP7DcYyQHLhwYewbnN92e2T9Nz4n81", "3DwUtJerhAjkm2UALCkQkNFnrPgFmMZ9hT"},
},
}
for _, test := range tests {
desc, err := nonstandard.OutputDescriptor([]byte(test.desc))
if err != nil {
t.Fatalf("%s: %v", test.desc, err)
}
for i, want := range test.receives {
got, err := Receive(desc, uint32(i))
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("descriptor %s: got address %d:%s, want %s", test.desc, i, got, want)
}
}
for i, want := range test.changes {
got, err := Change(desc, uint32(i))
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("descriptor %s: got change address %d:%s, want %s", test.desc, i, got, want)
}
}
}
}

420
backup/backup.go Normal file
View File

@ -0,0 +1,420 @@
// package backup implements the SeedHammer backup scheme.
package backup
import (
"errors"
"fmt"
"image"
"math"
"math/bits"
"reflect"
"strings"
"github.com/kortschak/qr"
"github.com/mineracks/seedhammer-v1-companion/bc/fountain"
"github.com/mineracks/seedhammer-v1-companion/bc/ur"
"github.com/mineracks/seedhammer-v1-companion/bc/urtypes"
"github.com/mineracks/seedhammer-v1-companion/bip39"
"github.com/mineracks/seedhammer-v1-companion/engrave"
"github.com/mineracks/seedhammer-v1-companion/font/vector"
"github.com/mineracks/seedhammer-v1-companion/seedqr"
)
type PlateSize int
const (
SquarePlate PlateSize = iota
LargePlate
)
func (p PlateSize) Dims() image.Point {
switch p {
case SquarePlate:
return image.Pt(85, 85)
case LargePlate:
return image.Pt(85, 134)
}
panic("unreachable")
}
type Seed struct {
Title string
KeyIdx int
Mnemonic bip39.Mnemonic
Keys int
MasterFingerprint uint32
Font *vector.Face
Size PlateSize
}
type Descriptor struct {
Descriptor urtypes.OutputDescriptor
KeyIdx int
Font *vector.Face
Size PlateSize
}
func dims(c engrave.Plan) (engrave.Plan, image.Point) {
b := engrave.Measure(c)
return engrave.Offset(-b.Min.X, -b.Min.Y, c), b.Size()
}
var ErrDescriptorTooLarge = errors.New("output descriptor is too large to backup")
const MaxTitleLen = 18
const outerMargin = 3
const innerMargin = 10
func TitleString(face *vector.Face, s string) string {
s = strings.ToUpper(s)
res := ""
for _, r := range s {
if _, _, valid := face.Decode(r); valid {
res += string(r)
}
if len(res) == MaxTitleLen {
break
}
}
return res
}
type engraveFunc func(plateDims image.Point) (engrave.Plan, error)
func engraveSide(scale int, size PlateSize, eng engraveFunc) (engrave.Plan, error) {
sz := size.Dims().Mul(scale)
side, err := eng(sz)
if err != nil {
return nil, err
}
bounds := engrave.Measure(side)
safetyMargin := image.Pt(outerMargin*scale, outerMargin*scale)
if !bounds.In(image.Rectangle{Min: safetyMargin, Max: sz.Sub(safetyMargin)}) {
return nil, ErrDescriptorTooLarge
}
return side, nil
}
func EngraveSeed(params engrave.Params, plate Seed) (engrave.Plan, error) {
return engraveSide(params.Millimeter, plate.Size, func(plateDims image.Point) (engrave.Plan, error) {
return frontSideSeed(params, plate, plateDims)
})
}
func EngraveDescriptor(params engrave.Params, plate Descriptor) (engrave.Plan, error) {
return engraveSide(params.Millimeter, plate.Size, func(plateDims image.Point) (engrave.Plan, error) {
urs := splitUR(plate.Descriptor, plate.KeyIdx)
return descriptorSide(params, plate.Font, urs, plate.Size, plateDims)
})
}
// splitUR searches for the appropriate seqNum in the [UR] encoding
// that makes m-of-n backups recoverable regardless of
// which m-sized subset is used. To achieve that, we're exploiting the
// fact that the UR encoding of a fragment can contain multiple fragments,
// xor'ed together.
//
// Schemes are implemented for backups where m == n - 1 and for 3-of-5.
//
// For m == n - 1, the data is split into m parts (seqLen in UR parlor), and m shares have parts
// assigned as follows:
//
// 1, 2, ..., m
//
// The final share contains the xor of all m parts.
//
// The scheme can trivially recover the data when selecting the m shares each with 1
// part. For all other selections, one share will be missing, say k, but we'll have the
// final plate with every part xor'ed together. So, k is derived by xor'ing (canceling) every
// part other than k into the combined part.
//
// Example: a 2-of-3 setup will have data split into 2 parts, with the 3 shares assigned parts
// like so: 1, 2, 1 ⊕ 2. Selecting the first two plates, the data is trivially recovered;
// otherwise we have one part, say 1, and the combined part. The other part, 2, is then recovered
// by xor'ing the one part with the combination: 1 ⊕ 1 ⊕ 2 = 2.
//
// For 3-of-5, the data is split into 6 parts, and each share will have two parts assigned.
//
// The assignment is as follows, where p1 and p2 denotes the two parts assigned to each share.
//
// share | p1 | p2
// 1 1 6 ⊕ 5 ⊕ 2
// 2 2 6 ⊕ 1 ⊕ 3
// 3 3 6 ⊕ 2 ⊕ 4
// 4 4 6 ⊕ 3 ⊕ 5
// 5 5 6 ⊕ 4 ⊕ 1
//
// That is, every share is assigned a part and the combination of the 6 part with the neighbour
// parts.
//
// [UR]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
func splitUR(desc urtypes.OutputDescriptor, keyIdx int) (urs []string) {
var shares [][]int
var seqLen int
m, n := desc.Threshold, len(desc.Keys)
switch {
case n-m <= 1:
// Optimal: 1 part per share, seqLen m.
seqLen = m
if keyIdx < m {
shares = [][]int{{keyIdx}}
} else {
all := make([]int, 0, m)
for i := 0; i < m; i++ {
all = append(all, i)
}
shares = [][]int{all}
}
case n == 4 && m == 2:
// Optimal, but 2 parts per share.
seqLen = m * 2
switch keyIdx {
case 0:
shares = [][]int{{0}, {1}}
case 1:
shares = [][]int{{2}, {3}}
case 2:
shares = [][]int{{0, 2}, {1, 3}}
case 3:
shares = [][]int{{0, 2, 1}, {1, 3, 2}}
}
case n == 5 && m == 3:
// Optimal, but 2 parts per share. There doesn't seem to exist an
// optimal scheme with 1 part per share.
seqLen = m * 2
second := []int{
n,
(keyIdx + n - 1) % n,
(keyIdx + 1) % n,
}
shares = [][]int{{keyIdx}, second}
default:
// Fallback: every share contains the complete data. It's only optimal
// for 1-of-n backups.
seqLen = 1
shares = [][]int{{0}}
}
data := desc.Encode()
check := fountain.Checksum(data)
for _, frag := range shares {
seqNum := fountain.SeqNumFor(seqLen, check, frag)
qr := strings.ToUpper(ur.Encode("crypto-output", data, seqNum, seqLen))
urs = append(urs, qr)
}
return
}
func Recoverable(desc urtypes.OutputDescriptor) bool {
var shares [][]string
for k := range desc.Keys {
shares = append(shares, splitUR(desc, k))
}
// Count to all bit patterns of n length, choose the ones with
// m bits.
allPerm := uint64(1)<<len(desc.Keys) - 1
for c := uint64(1); c <= allPerm; c++ {
if bits.OnesCount64(c) != desc.Threshold {
continue
}
c := c
d := new(ur.Decoder)
for c != 0 {
share := bits.TrailingZeros64(c)
c &^= 1 << share
for _, ur := range shares[share] {
d.Add(ur)
}
}
typ, enc, err := d.Result()
if err != nil {
return false
}
if enc == nil {
return false
}
got, err := urtypes.Parse(typ, enc)
if err != nil {
return false
}
gotDesc := got.(urtypes.OutputDescriptor)
gotDesc.Title = desc.Title
if !reflect.DeepEqual(gotDesc, desc) {
return false
}
}
return true
}
const plateFontSize = 4.1
const plateFontSizeUR = 3.8
const plateSmallFontSize = 3.
func frontSideSeed(params engrave.Params, plate Seed, plateDims image.Point) (engrave.Plan, error) {
constant := engrave.NewConstantStringer(plate.Font, params.F(plateFontSize), bip39.ShortestWord, bip39.LongestWord)
var cmds []engrave.Plan
cmd := func(c engrave.Plan) {
cmds = append(cmds, c)
}
maxCol1 := 16
maxCol2 := 4
endCol1 := maxCol1
if endCol1 > len(plate.Mnemonic) {
endCol1 = len(plate.Mnemonic)
}
col1, col1b := dims(wordColumn(constant, plate.Font, params.F(plateFontSize), plate.Mnemonic, 0, endCol1))
// Engrave version, mfp and page.
const version = "V1"
innerMargin := params.I(innerMargin)
metaMargin := params.I(4)
page := fmt.Sprintf("%d/%d", plate.KeyIdx+1, plate.Keys)
mfp := strings.ToUpper(fmt.Sprintf("%.8x", plate.MasterFingerprint))
{
offy := (plateDims.Y-col1b.Y)/2 - metaMargin
pagec, sz := dims(engrave.String(plate.Font, params.F(plateSmallFontSize), page).Engrave)
cmd(engrave.Offset(innerMargin, offy-sz.Y, pagec))
mfpc, sz := dims(engrave.String(plate.Font, params.F(plateSmallFontSize), mfp).Engrave)
cmd(engrave.Offset((plateDims.X-sz.X)/2, offy-sz.Y, mfpc))
txt, sz := dims(engrave.String(plate.Font, params.F(plateSmallFontSize), version).Engrave)
cmd(engrave.Offset(plateDims.X-sz.X-innerMargin, offy-sz.Y, txt))
}
// Engrave column 1.
cmd(engrave.Offset(innerMargin, (plateDims.Y-col1b.Y)/2, col1))
// Engrave (top of) column 2.
endCol2 := endCol1 + maxCol2
if endCol2 > len(plate.Mnemonic) {
endCol2 = len(plate.Mnemonic)
}
col2, _ := dims(wordColumn(constant, plate.Font, params.F(plateFontSize), plate.Mnemonic, endCol1, endCol2))
cmd(engrave.Offset(params.I(44), (plateDims.Y-col1b.Y)/2, col2))
// Engrave seed QR.
qrCmd, err := engrave.ConstantQR(params.StrokeWidth, 3, qr.Q, seedqr.CompactQR(plate.Mnemonic))
if err != nil {
return nil, err
}
qr, sz := dims(qrCmd)
cmd(engrave.Offset(params.I(60)-sz.X/2, (plateDims.Y-sz.Y)/2, qr))
{
// Engrave bottom of column 2.
col2, col2b := dims(wordColumn(constant, plate.Font, params.F(plateFontSize), plate.Mnemonic, endCol2, len(plate.Mnemonic)))
cmd(engrave.Offset(params.I(44), (plateDims.Y+col1b.Y)/2-col2b.Y, col2))
}
// Engrave title.
title := strings.ToUpper(plate.Title)
{
offy := (plateDims.Y+col1b.Y)/2 + metaMargin
title, sz := dims(engrave.String(plate.Font, params.F(plateSmallFontSize), title).Engrave)
cmd(engrave.Offset((plateDims.X-sz.X)/2, offy, title))
}
all := engrave.Commands(cmds...)
if plate.Size == LargePlate {
// Avoid the middle holes.
return engrave.Offset(0, params.F(24.5), all), nil
}
return all, nil
}
func wordColumn(constant *engrave.ConstantStringer, font *vector.Face, fontSize int, mnemonic bip39.Mnemonic, start, end int) engrave.Plan {
var cmds []engrave.Plan
y := 0
for i := start; i < end; i++ {
num := engrave.String(font, fontSize, fmt.Sprintf("%2d ", i+1))
d := num.Measure()
w := mnemonic[i]
word := strings.ToUpper(bip39.LabelFor(w))
txt := constant.String(word)
cmds = append(cmds,
engrave.Offset(0, y, num.Engrave),
engrave.Offset(d.X, y, txt),
)
y += d.Y
}
return engrave.Commands(cmds...)
}
func descriptorSide(params engrave.Params, fnt *vector.Face, urs []string, size PlateSize, plateDims image.Point) (engrave.Plan, error) {
var cmds []engrave.Plan
cmd := func(c engrave.Plan) {
cmds = append(cmds, c)
}
fontSize := params.F(plateFontSizeUR)
str := func(s string) engrave.Plan {
return engrave.String(fnt, fontSize, s).Engrave
}
// Compute character width, assuming the font is fixed width.
charWidthf, _, ok := fnt.Decode('W')
if !ok {
panic("W not in font")
}
charWidth := int(float32(charWidthf*fontSize) / float32(fnt.Metrics().Height))
margin := params.I(outerMargin)
innerMargin := params.I(innerMargin)
if size == LargePlate {
margin = innerMargin
}
holeChars := int(math.Ceil(float64(innerMargin-margin) / float64(charWidth)))
holeLines := int(math.Ceil(float64(innerMargin-margin) / float64(fontSize)))
width := plateDims.X - 2*margin
charPerLine := int(width / charWidth)
offy := params.I(outerMargin)
for i, ur := range urs {
qrcmd, err := engrave.QR(params.StrokeWidth, 2, qr.M, []byte(ur))
if err != nil {
return nil, err
}
qr, qrsz := dims(qrcmd)
qrBorder := params.I(2)
charPerQRLine := (width - 2*qrBorder - qrsz.X) / charWidth
qrLines := (qrsz.Y + 2*qrBorder + fontSize - 1) / fontSize
qrLineStart := holeLines
lineno := 0
for len(ur) > 0 {
n := charPerLine
offx := 0
isQRLine := qrLineStart <= lineno && lineno < qrLineStart+qrLines
if isQRLine {
n = charPerQRLine
}
// Avoid screw holes on the smaller plates on the first and last lines.
holeLine := offy+lineno*fontSize < innerMargin ||
offy+(lineno+1)*fontSize > plateDims.Y-innerMargin
if holeLine {
if !isQRLine {
// End of line.
n -= holeChars
}
// Beginning of line.
n -= holeChars
offx = holeChars * charWidth
}
if n < 1 {
n = 1
}
if n > len(ur) {
n = len(ur)
}
s := ur[:n]
ur = ur[n:]
cmd(engrave.Offset(offx+margin, offy+lineno*fontSize, str(s)))
lineno++
}
qrx := plateDims.X - qrsz.X - margin - qrBorder
qry := qrLineStart*fontSize + (qrLines*fontSize-qrsz.Y)/2
cmd(engrave.Offset(qrx, offy+qry, qr))
offy += lineno * fontSize
if i != len(urs)-1 {
// Space UR sections.
offy += params.I(1)
}
}
return engrave.Commands(cmds...), nil
}

294
backup/backup_test.go Executable file
View File

@ -0,0 +1,294 @@
package backup
import (
"bytes"
"errors"
"flag"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"testing"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/mineracks/seedhammer-v1-companion/bc/urtypes"
"github.com/mineracks/seedhammer-v1-companion/bip32"
"github.com/mineracks/seedhammer-v1-companion/bip39"
"github.com/mineracks/seedhammer-v1-companion/driver/mjolnir"
"github.com/mineracks/seedhammer-v1-companion/engrave"
"github.com/mineracks/seedhammer-v1-companion/font/constant"
)
var update = flag.Bool("update", false, "update golden files")
func TestEngraveErrors(t *testing.T) {
p2wsh := []uint32{
hdkeychain.HardenedKeyStart + 48,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 2,
}
tests := []struct {
threshold int
keys int
side int
path []uint32
seedLen int
err error
}{
{1, 5, 0, p2wsh, 24, ErrDescriptorTooLarge},
}
for i, test := range tests {
t.Run(fmt.Sprintf("error-%d", i), func(t *testing.T) {
desc := urtypes.OutputDescriptor{
Title: "Satoshi Stash",
Script: urtypes.P2WSH,
Threshold: test.threshold,
Type: urtypes.SortedMulti,
Keys: make([]urtypes.KeyDescriptor, test.keys),
}
_, descDesc := genTestPlate(t, desc, test.path, test.seedLen, 0, LargePlate)
const ppmm = 4
_, err := EngraveDescriptor(mjolnir.Params, descDesc)
if err == nil {
t.Fatalf("no error reported by Engrave, expected %v", test.err)
}
if !errors.Is(err, test.err) {
t.Fatalf("got error %v, expected %v", err, test.err)
}
})
}
}
func TestEngrave(t *testing.T) {
tests := []struct {
threshold int
keys int
side int
script urtypes.Script
seedLen int
size PlateSize
}{
// Seed only variants.
{1, 1, 0, urtypes.P2SH, 12, SquarePlate},
{1, 1, 0, urtypes.P2TR, 24, SquarePlate},
{1, 1, 1, urtypes.P2WPKH, 24, SquarePlate},
{1, 1, 0, urtypes.P2WSH, 12, SquarePlate},
{1, 1, 0, urtypes.P2WSH, 24, SquarePlate},
{3, 5, 1, urtypes.P2SH_P2WSH, 24, LargePlate},
// Descriptor variants, seed side.
{1, 1, 1, urtypes.P2SH_P2WSH, 12, SquarePlate},
{1, 1, 1, urtypes.P2SH_P2WSH, 24, SquarePlate},
{1, 2, 1, urtypes.P2SH_P2WSH, 12, LargePlate},
{3, 5, 1, urtypes.P2SH_P2WSH, 24, LargePlate},
// Descriptor side.
{1, 1, 0, urtypes.P2SH_P2WSH, 12, SquarePlate},
{1, 2, 0, urtypes.P2SH_P2WSH, 12, LargePlate},
{2, 3, 0, urtypes.P2SH_P2WSH, 12, SquarePlate},
{3, 5, 0, urtypes.P2SH_P2WSH, 12, LargePlate},
{9, 10, 0, urtypes.P2SH_P2WSH, 12, SquarePlate},
}
for i, test := range tests {
i, test := i, test
name := fmt.Sprintf("%d-%d-of-%d-%d-words", i, test.threshold, test.keys, test.seedLen)
t.Run(name, func(t *testing.T) {
t.Parallel()
desc := urtypes.OutputDescriptor{
Title: "Satoshi Stash",
Script: test.script,
Threshold: test.threshold,
Type: urtypes.Singlesig,
Keys: make([]urtypes.KeyDescriptor, test.keys),
}
if len(desc.Keys) > 1 {
desc.Type = urtypes.SortedMulti
}
path := desc.Script.DerivationPath()
seedDesc, descDesc := genTestPlate(t, desc, path, test.seedLen, 0, test.size)
const ppmm = 4
params := mjolnir.Params
var side engrave.Plan
var err error
if test.side == 0 {
side, err = EngraveDescriptor(params, descDesc)
} else {
side, err = EngraveSeed(params, seedDesc)
}
if err != nil {
t.Fatal(err)
}
sz := test.size.Dims().Mul(ppmm)
bounds := image.Rectangle{
Max: sz,
}
name := fmt.Sprintf("plate-%d-side-%d-%d-of-%d-words-%d.png", i, test.side, desc.Threshold, len(desc.Keys), test.seedLen)
golden := filepath.Join("testdata", name)
got := image.NewAlpha(bounds)
r := engrave.NewRasterizer(got, bounds, float32(ppmm)/float32(params.Millimeter), params.StrokeWidth*ppmm/params.Millimeter)
se := side
se(r.Command)
r.Rasterize()
// Binarize to minimize golden image sizes.
for i, p := range got.Pix {
if p < 128 {
p = 0
} else {
p = 255
}
got.Pix[i] = p
}
if *update {
var buf bytes.Buffer
if err := png.Encode(&buf, got); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(golden, buf.Bytes(), 0o640); err != nil {
t.Fatal(err)
}
return
}
f, err := os.Open(golden)
if err != nil {
t.Fatal(err)
}
want, _, err := image.Decode(f)
if err != nil {
t.Fatal(err)
}
if w, g := want.Bounds().Size(), got.Bounds().Size(); w != g {
t.Fatalf("golden image bounds mismatch: got %v, want %v", g, w)
}
mismatches := 0
pixels := 0
width, height := want.Bounds().Dx(), want.Bounds().Dy()
gotOff := bounds.Min
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
wanty16, _, _, _ := want.At(x, y).RGBA()
want := wanty16 != 0
got := got.AlphaAt(gotOff.X+x, gotOff.Y+y).A != 0
if want {
pixels++
}
if got != want {
mismatches++
}
}
}
const maxErrors = 65
if mismatches > maxErrors {
t.Errorf("%d/%d pixels golden image mismatches", mismatches, pixels)
}
})
}
}
func TestSplitUR(t *testing.T) {
t.Parallel()
maxShares := 15
if testing.Short() {
maxShares = 10
}
for n := 1; n <= maxShares; n++ {
n := n
name := fmt.Sprintf("%d-shares", n)
t.Run(name, func(t *testing.T) {
t.Parallel()
for m := 1; m <= n; m++ {
desc := urtypes.OutputDescriptor{
Title: "Some title",
Script: urtypes.P2WSH,
Threshold: m,
Type: urtypes.Singlesig,
Keys: make([]urtypes.KeyDescriptor, n),
}
if len(desc.Keys) > 1 {
desc.Type = urtypes.SortedMulti
}
genTestPlate(t, desc, desc.Script.DerivationPath(), 12, 0, LargePlate)
if !Recoverable(desc) {
t.Errorf("%d-of-%d: failed to recover", m, n)
}
}
})
}
}
func TestTitleString(t *testing.T) {
tests := []struct {
test string
title string
}{
{"Satoshi's Wallet", "SATOSHI'S WALLET"},
{"Anø de:Æby09 . asd asd asd as das d asd asdf sdf s fd", "AN DE:BY09 . ASD A"},
{"Æg", "G"},
{"🤡 💩", " "},
{"$€#,", "#,"},
}
for _, test := range tests {
s := TitleString(constant.Font, test.test)
if s != test.title {
t.Fatalf("got %q, wanted %q", s, test.title)
}
}
}
func genTestPlate(t *testing.T, desc urtypes.OutputDescriptor, path []uint32, seedlen int, keyIdx int, plateSize PlateSize) (Seed, Descriptor) {
var mnemonic bip39.Mnemonic
for i := range desc.Keys {
m := make(bip39.Mnemonic, seedlen)
for j := range m {
m[j] = bip39.Word(i*seedlen + j)
}
m = m.FixChecksum()
seed := bip39.MnemonicSeed(m, "")
network := &chaincfg.MainNetParams
mk, err := hdkeychain.NewMaster(seed, network)
if err != nil {
t.Fatal(err)
}
if len(path) == 0 {
// Ensure the master fingerprint is derived.
path = urtypes.Path{0}
}
mfp, xpub, err := bip32.Derive(mk, path)
if err != nil {
t.Fatal(err)
}
pub, err := xpub.ECPubKey()
if err != nil {
t.Fatal(err)
}
desc.Keys[i] = urtypes.KeyDescriptor{
Network: network,
MasterFingerprint: mfp,
DerivationPath: path,
ParentFingerprint: xpub.ParentFingerprint(),
ChainCode: xpub.ChainCode(),
KeyData: pub.SerializeCompressed(),
}
if i == keyIdx {
mnemonic = m
}
}
return Seed{
Title: desc.Title,
KeyIdx: keyIdx,
Mnemonic: mnemonic,
Keys: len(desc.Keys),
MasterFingerprint: desc.Keys[keyIdx].MasterFingerprint,
Font: constant.Font,
Size: plateSize,
}, Descriptor{
Descriptor: desc,
KeyIdx: keyIdx,
Font: constant.Font,
Size: plateSize,
}
}

View File

@ -1,19 +1,18 @@
// Package backup defines SeedHammer v1 plate dimensions and layout // Package backup defines SeedHammer v1 plate dimensions and the backup-
// constants. // encoding scheme that turns wallet data into engrave-ready descriptions.
// //
// Three plate types are supported, matching upstream's stainless plate SKUs: // Three plate types match upstream's stainless plate SKUs:
// //
// SmallPlate 85 × 55 mm — single seed (12 or 24 words) // SmallPlate 85 × 55 mm — single seed (12 or 24 words)
// SquarePlate 85 × 85 mm — single seed + title // SquarePlate 85 × 85 mm — single seed + title
// LargePlate 85 × 134 mm — seed + descriptor for multisig // LargePlate 85 × 134 mm — seed + descriptor for multisig
// //
// The engraver's origin sits 97mm in the X axis from the plate edge (a // LIFTED from upstream seedhammer/seedhammer at v1.3.0
// fixed offset of the physical machine). outerMargin = 3 mm and // (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e). NOT lifted from
// innerMargin = 10 mm define the engrave-safe area. // Gangleri42's fork because that fork strips out SmallPlate (SH-II
// doesn't support it) and removes the UR-code multi-plate encoding
// (SH-II uses NFC payload instead).
// //
// All constants are mm; the engrave/ package converts to machine steps // backup_test.go from upstream pulls in mjolnir + engrave + bip32 — we've
// (1 step ≈ 0.00796 mm) at command-generation time. // renamed it to backup_test.go.deferred until those packages land.
//
// Status: STUB — to be lifted verbatim from upstream's backup/backup.go
// at v1.3.0. The constants are stable across all v1.x releases.
package backup package backup

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

120
bc/bytewords/bytewords.go Normal file
View File

@ -0,0 +1,120 @@
// Package bytewords implements the the bytewords standard
// as described in [BCR-2020-012].
//
// [BCR-2020-012]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
package bytewords
import (
"encoding/binary"
"errors"
"hash/crc32"
)
type byteview interface {
~string | ~[]byte
}
func Encode(data []byte) string {
buf := make([]byte, 0, (len(data)+4)*2)
for _, b := range data {
i := int(b) * 2
buf = append(buf, abbrev[i:i+2]...)
}
check := crc32.ChecksumIEEE(data)
var checkb [4]byte
binary.BigEndian.PutUint32(checkb[:], check)
for _, b := range checkb {
i := int(b) * 2
buf = append(buf, abbrev[i:i+2]...)
}
return string(buf)
}
func Decode[T byteview](src T) ([]byte, error) {
if len(src)%2 == 1 {
return nil, errors.New("truncated input")
}
dst := make([]byte, len(src)/2)
if len(dst) < 4 {
return nil, errors.New("input too short")
}
for i := range dst {
w, ok := lookup(src[i*2], src[i*2+1])
if !ok {
return nil, errors.New("invalid word")
}
dst[i] = w
}
res := dst[:len(dst)-4]
got := binary.BigEndian.Uint32(dst[len(dst)-4:])
want := crc32.ChecksumIEEE(res)
if got != want {
return nil, errors.New("crc32 checksum mismatch")
}
return res, nil
}
func lookup(l1, l2 byte) (byte, bool) {
idx := l1 - 'a'
if int(idx) >= len(firstLetters) {
return 0, false
}
start := firstLetters[l1-'a']
for i := int(start); i < len(abbrev)/2; i++ {
w0, w3 := abbrev[i*2], abbrev[i*2+1]
if w0 != l1 {
break
}
if w3 == l2 {
return byte(i), true
}
}
return 0, false
}
var firstLetters [26]uint8
func init() {
var letter byte = 'a' - 1
for i := 0; i < len(abbrev)/2; i++ {
if l1 := abbrev[i*2]; l1 != letter {
letter = l1
firstLetters[letter-'a'] = uint8(i)
}
}
}
// abbrev contains the two-letter abbreviations for the bytewords word list:
// able, acid, also, apex, aqua, arch, atom, aunt,
// away, axis, back, bald, barn, belt, beta, bias,
// blue, body, brag, brew, bulb, buzz, calm, cash,
// cats, chef, city, claw, code, cola, cook, cost,
// crux, curl, cusp, cyan, dark, data, days, deli,
// dice, diet, door, down, draw, drop, drum, dull,
// duty, each, easy, echo, edge, epic, even, exam,
// exit, eyes, fact, fair, fern, figs, film, fish,
// fizz, flap, flew, flux, foxy, free, frog, fuel,
// fund, gala, game, gear, gems, gift, girl, glow,
// good, gray, grim, guru, gush, gyro, half, hang,
// hard, hawk, heat, help, high, hill, holy, hope,
// horn, huts, iced, idea, idle, inch, inky, into,
// iris, iron, item, jade, jazz, join, jolt, jowl,
// judo, jugs, jump, junk, jury, keep, keno, kept,
// keys, kick, kiln, king, kite, kiwi, knob, lamb,
// lava, lazy, leaf, legs, liar, limp, lion, list,
// logo, loud, love, luau, luck, lung, main, many,
// math, maze, memo, menu, meow, mild, mint, miss,
// monk, nail, navy, need, news, next, noon, note,
// numb, obey, oboe, omit, onyx, open, oval, owls,
// paid, part, peck, play, plus, poem, pool, pose,
// puff, puma, purr, quad, quiz, race, ramp, real,
// redo, rich, road, rock, roof, ruby, ruin, runs,
// rust, safe, saga, scar, sets, silk, skew, slot,
// soap, solo, song, stub, surf, swan, taco, task,
// taxi, tent, tied, time, tiny, toil, tomb, toys,
// trip, tuna, twin, ugly, undo, unit, urge, user,
// vast, very, veto, vial, vibe, view, visa, void,
// vows, wall, wand, warm, wasp, wave, waxy, webs,
// what, when, whiz, wolf, work, yank, yawn, yell,
// yoga, yurt, zaps, zero, zest, zinc, zone, zoom.
const abbrev = "aeadaoaxaaahamatayasbkbdbnbtbabsbebybgbwbbbzcmchcscfcycwcecackctcxclcpcndkdadsdidedtdrdndwdpdmdldyeheyeoeeecenemetesftfrfnfsfmfhfzfpfwfxfyfefgflfdgagegrgsgtglgwgdgygmgughgohfhghdhkhthphhhlhyhehnhsidiaieihiyioisinimjejzjnjtjljojsjpjkjykpkoktkskkknkgkekikblblalylflslrlplnltloldlelulklgmnmymhmemomumwmdmtmsmknlnyndnsntnnnenboyoeotoxonolospdptpkpypspmplpepfpaprqdqzrerprlrorhrdrkrfryrnrsrtsesasrssskswstspsosgsbsfsntotktitttdtetytltbtstptatnuyuoutueurvtvyvovlvevwvavdvswlwdwmwpwewywswtwnwzwfwkykynylyaytzszoztzczezm"

View File

@ -0,0 +1,62 @@
package bytewords
import (
"bytes"
"encoding/hex"
"testing"
)
func TestRoundtrip(t *testing.T) {
want := make([]byte, 255)
for i := range want {
want[i] = byte(i)
}
enc := Encode(want)
got, err := Decode(enc)
if err != nil {
t.Fatalf("%v encoded to %s, but failed to decode: %v", want, enc, err)
}
if !bytes.Equal(want, got) {
t.Errorf("%v encoded to %s, but roundtripped to %v", want, enc, got)
}
}
func TestEncoding(t *testing.T) {
tests := []struct {
bw string
wanthex string
error bool
}{
{"aeadaolazmjendeoti", "00010280ff", false},
{"taaddwoeadgdstaslplabghydrpfmkbggufgludprfgmaotpiecffltntddwgmrp", "d9012ca20150c7098580125e2ab0981253468b2dbc5202d8641947da", false},
// Bad checksum.
{"taaddwoeadgdstaslplabghydrpfmkbggufgludprfgmaotpiecffltntddwgmrs", "", true},
{"", "", true},
}
for _, test := range tests {
got, err := Decode(test.bw)
if err != nil {
if !test.error {
t.Errorf("failed to decode %q: %v", test.bw, err)
}
} else {
if test.error {
t.Errorf("unexpected successful decoding of %q", test.bw)
}
}
if test.error {
continue
}
want, err := hex.DecodeString(test.wanthex)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Errorf("decoding %q got %#x, expected %#x", test.bw, got, want)
}
roundtrip := Encode(want)
if roundtrip != test.bw {
t.Errorf("encoding %s got %s, expected %s", test.wanthex, roundtrip, test.bw)
}
}
}

14
bc/doc.go Normal file
View File

@ -0,0 +1,14 @@
// Package bc is the parent of upstream's Blockchain Commons code: UR
// (Uniform Resources) encoded payloads, fountain codes for multi-frame
// transport, bytewords for human-checksummable encoding, plus xoshiro256
// for deterministic ranom-stream generation.
//
// All five subpackages (ur, fountain, bytewords, urtypes, xoshiro256) live
// here. The v1 controller uses these to encode multi-plate backup data
// into animated QR codes that fit through the WaveShare LCD's small frame.
//
// LIFTED from upstream seedhammer/seedhammer at v1.3.0
// (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e). Gangleri42's fork
// has the same code modernized for Go 1.23+ idioms; functionally identical.
// We use upstream's v1.3.0 because that's what backup/ was tested against.
package bc

338
bc/fountain/fountain.go Normal file
View File

@ -0,0 +1,338 @@
// 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]
}
}

View File

@ -0,0 +1,163 @@
package fountain
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"reflect"
"sort"
"testing"
"github.com/mineracks/seedhammer-v1-companion/bc/xoshiro256"
)
func TestDecoding(t *testing.T) {
tests := []struct {
parts []string
want string
seqLen int
seqNums []int
}{
{
[]string{
"85010319022b1a2f972da558b95902282320426c756557616c6c6574204d756c74697369672073657475702066696c650a2320746869732066696c6520636f6e7461696e73206f6e6c79207075626c6963206b65797320616e64206973207361666520746f0a23206469737472696275746520616d6f6e6720636f7369676e6572730a230a4e616d653a2073680a506f6c6963793a2032206f6620330a44657269766174696f6e3a206d2f3438272f30272f30272f32270a466f726d61743a2050325753480a",
"85020319022b1a2f972da558b90a35413038303445333a207870756236463134384c6e6a556847724866454e36506138566b7746384c36464a7159414c78416b75486661636656684d4c5659344d527555564d7872397067754176363744487831594678716f4b4e38733451665a74443973523278524366665471693945384669464c41596b380a0a44443446414445453a207870756236446e656469557559385063633646656a385974325a6e745043794664706248426b4e56374561776573524d626336",
"85030319022b1a2f972da558b969394d4b4b4d684b4576344a4d4d7a77444a636b615634637a42764e646336696b774c695a716455714d64355a4b5147596151543463584d65566a660a0a39424143443543303a2078707562364565667243724d416475684e776e734862336441733844595a53773466363357795236446145427955486a777650446468637a6a31354679424247347462454a74663476524b5476316e67355350506e57763150766531663135454a66694259356f59444e36564c45430a0a",
},
"5902282320426c756557616c6c6574204d756c74697369672073657475702066696c650a2320746869732066696c6520636f6e7461696e73206f6e6c79207075626c6963206b65797320616e64206973207361666520746f0a23206469737472696275746520616d6f6e6720636f7369676e6572730a230a4e616d653a2073680a506f6c6963793a2032206f6620330a44657269766174696f6e3a206d2f3438272f30272f30272f32270a466f726d61743a2050325753480a0a35413038303445333a207870756236463134384c6e6a556847724866454e36506138566b7746384c36464a7159414c78416b75486661636656684d4c5659344d527555564d7872397067754176363744487831594678716f4b4e38733451665a74443973523278524366665471693945384669464c41596b380a0a44443446414445453a207870756236446e656469557559385063633646656a385974325a6e745043794664706248426b4e56374561776573524d62633669394d4b4b4d684b4576344a4d4d7a77444a636b615634637a42764e646336696b774c695a716455714d64355a4b5147596151543463584d65566a660a0a39424143443543303a2078707562364565667243724d416475684e776e734862336441733844595a53773466363357795236446145427955486a777650446468637a6a31354679424247347462454a74663476524b5476316e67355350506e57763150766531663135454a66694259356f59444e36564c45430a0a",
3,
[]int{1, 2, 3},
},
{
[]string{
"85190571021901671a16c6621158b4c36133f5ca04a4efa107339a9e31069fad2b597ce0dab85c2ac34ea8c33b716b56ce8d0e5d196e908b2cd339e572d4b092d55a726ca9b623dfe01699d89d365207dbd6d05be4f0e0791c73fb5fae547df74c39957d21d81616d3d80b2a6f731550356242d31f79d27534ad2060b3bc11667dbfabce24b8515fbd6726ed918d3944a913974a6bbf3260f27b68c786df273de82e727696801112d6d33c14f972761fab67badf8409c53ed198234786e5ecd70e4fd1",
"8519057d021901671a16c6621158b4c36133f5ca04a4efa107339a9e31069fad2b597ce0dab85c2ac34ea8c33b716b56ce8d0e5d196e908b2cd339e572d4b092d55a726ca9b623dfe01699d89d365207dbd6d05be4f0e0791c73fb5fae547df74c39957d21d81616d3d80b2a6f731550356242d31f79d27534ad2060b3bc11667dbfabce24b8515fbd6726ed918d3944a913974a6bbf3260f27b68c786df273de82e727696801112d6d33c14f972761fab67badf8409c53ed198234786e5ecd70e4fd1",
"85190581021901671a16c6621158b41a60a22ccb9306eea305b0439f1ea09d5928015de373811605d90131a20100020006d90130a301881830f500f500f502f5021add4fadee0304081a22969377d9012fa602f403582102fb72507fc20ddba92991b17c4bb466130ad93a886e73175033bb43e3bc785a6d04582095b34913937fa5f1c6205b525bb57de1517625e04586b595be68e71362d3edc505d90131a20100020006d90130a301881830f500f500f502f5021a9bacd5c00304081a97ec38f900",
},
"d90191d90197a201020283d9012fa602f403582103a9394a2f1a4f99613a716956c8540f6dba6f18931c2639107221b267d740af23045820dbe80cbb4e0e418b06f470d2afe7a8c17be701ab206c59a65e65a824016a6c7005d90131a20100020006d90130a301881830f500f500f502f5021a5a0804e30304081ac7bce7a8d9012fa602f4035821022196adc25fde169fe92e70769059102275d2b40cc98776eaab92b82a86135e92045820438eff7b3b36b6d11a60a22ccb9306eea305b0439f1ea09d5928015de373811605d90131a20100020006d90130a301881830f500f500f502f5021add4fadee0304081a22969377d9012fa602f403582102fb72507fc20ddba92991b17c4bb466130ad93a886e73175033bb43e3bc785a6d04582095b34913937fa5f1c6205b525bb57de1517625e04586b595be68e71362d3edc505d90131a20100020006d90130a301881830f500f500f502f5021a9bacd5c00304081a97ec38f9",
2,
[]int{1393, 1405, 1409},
},
{
[]string{
"8505091901031aeda0ae73581dd60b3ec4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f",
"850c091901031aeda0ae73581db7808bff2e4ccec832643eed6ff0af2598cfc3e31a52fe92e2e380b829",
"850d091901031aeda0ae73581d967bd87a541717f538efe54f485b524df71fa3fba8b608a717165b8240",
"850e091901031aeda0ae73581db1fef1e29ee79c118af6f09c736d28a630240a268d731476c010889334",
"850f091901031aeda0ae73581df690a82dffe4bf0bb344b560b48b526c8e96ebb8dc5ac74c0f05b1f427",
"8510091901031aeda0ae73581dee4a760e94d565cdb186ca5f9c79669d58fbd76ace6bd8bfd1937db7bf",
"8511091901031aeda0ae73581d988d3a03f5afeec0b45e2bd89ad468692090d61c087689dcc0a3636363",
"8512091901031aeda0ae73581d590100916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e",
"8513091901031aeda0ae73581d287d220470a36cac2a0e8532e97f26a06900bbdfc80c204c8d3ae0c36e",
"8514091901031aeda0ae73581df8adb7348a03b1ccc0ba7a1942746c51382e8af075774e8ab0b7d9d9fc",
},
"590100916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3ccba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f965e25ee29039fdf8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3ec4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f595e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff173f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d",
9,
[]int{5, 12, 13, 14, 15, 16, 17, 18, 19, 20},
},
{
[]string{
"850002011ad202ef8d4100",
"850102011ad202ef8d4100",
},
"00",
2,
[]int{0, 1},
},
}
for _, test := range tests {
w, err := hex.DecodeString(test.want)
if err != nil {
t.Fatal(err)
}
for i, seqNum := range test.seqNums {
got := Encode(w, seqNum, test.seqLen)
gotHex := hex.EncodeToString(got)
if test.parts[i] != gotHex {
t.Errorf("seqNum %d of %s is %s expected %s", seqNum, test.want, gotHex, test.parts[i])
}
}
var d Decoder
for _, f := range test.parts {
data, err := hex.DecodeString(f)
if err != nil {
t.Fatal(err)
}
if err := d.Add(data); err != nil {
t.Error(err)
}
}
v, err := d.Result()
if err != nil {
t.Error(err)
}
if v == nil {
t.Fatal("not enough fragments to decode")
}
if !bytes.Equal(w, v) {
t.Error("mismatched decoded value")
}
}
}
func TestChooseDegree(t *testing.T) {
const seqLen = 11
var degrees []int
for nonce := 1; nonce <= 200; nonce++ {
rng := new(xoshiro256.Source)
h := sha256.Sum256([]byte(fmt.Sprintf("Wolf-%d", nonce)))
rng.Seed(h)
degrees = append(degrees, chooseDegree(seqLen, rng))
}
want := []int{11, 3, 6, 5, 2, 1, 2, 11, 1, 3, 9, 10, 10, 4, 2, 1, 1, 2, 1, 1, 5, 2, 4, 10, 3, 2, 1, 1, 3, 11, 2, 6, 2, 9, 9, 2, 6, 7, 2, 5, 2, 4, 3, 1, 6, 11, 2, 11, 3, 1, 6, 3, 1, 4, 5, 3, 6, 1, 1, 3, 1, 2, 2, 1, 4, 5, 1, 1, 9, 1, 1, 6, 4, 1, 5, 1, 2, 2, 3, 1, 1, 5, 2, 6, 1, 7, 11, 1, 8, 1, 5, 1, 1, 2, 2, 6, 4, 10, 1, 2, 5, 5, 5, 1, 1, 4, 1, 1, 1, 3, 5, 5, 5, 1, 4, 3, 3, 5, 1, 11, 3, 2, 8, 1, 2, 1, 1, 4, 5, 2, 1, 1, 1, 5, 6, 11, 10, 7, 4, 7, 1, 5, 3, 1, 1, 9, 1, 2, 5, 5, 2, 2, 3, 10, 1, 3, 2, 3, 3, 1, 1, 2, 1, 3, 2, 2, 1, 3, 8, 4, 1, 11, 6, 3, 1, 1, 1, 1, 1, 3, 1, 2, 1, 10, 1, 1, 8, 2, 7, 1, 2, 1, 9, 2, 10, 2, 1, 3, 4, 10}
if !reflect.DeepEqual(degrees, want) {
t.Errorf("mismatched degrees")
}
}
func TestChooseFragments(t *testing.T) {
const seqLen = 11
const checksum = 790229947
var indexes [][]int
for seqNum := uint32(1); seqNum <= 30; seqNum++ {
set := chooseFragments(seqNum, seqLen, checksum)
sort.Ints(set)
indexes = append(indexes, set)
}
want := [][]int{
{0},
{1},
{2},
{3},
{4},
{5},
{6},
{7},
{8},
{9},
{10},
{9},
{2, 5, 6, 8, 9, 10},
{8},
{1, 5},
{1},
{0, 2, 4, 5, 8, 10},
{5},
{2},
{2},
{0, 1, 3, 4, 5, 7, 9, 10},
{0, 1, 2, 3, 5, 6, 8, 9, 10},
{0, 2, 4, 5, 7, 8, 9, 10},
{3, 5},
{4},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{0, 1, 3, 4, 5, 6, 7, 9, 10},
{6},
{5, 6},
{7},
}
if !reflect.DeepEqual(indexes, want) {
t.Errorf("mismatched fragment indexes")
}
}

88
bc/ur/ur.go Normal file
View File

@ -0,0 +1,88 @@
// Package ur implements the Uniform Resources (UR) encoding
// specified in [BCR-2020-005].
//
// [BCR-2020-005]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
package ur
import (
"errors"
"fmt"
"strings"
"github.com/mineracks/seedhammer-v1-companion/bc/bytewords"
"github.com/mineracks/seedhammer-v1-companion/bc/fountain"
)
func Encode(_type string, message []byte, seqNum, seqLen int) string {
if seqLen == 1 {
return fmt.Sprintf("ur:%s/%s", _type, bytewords.Encode(message))
}
data := fountain.Encode(message, seqNum, seqLen)
return fmt.Sprintf("ur:%s/%d-%d/%s", _type, seqNum, seqLen, bytewords.Encode(data))
}
type Decoder struct {
typ string
data []byte
fountain fountain.Decoder
}
func (d *Decoder) Progress() float32 {
if d.data != nil {
return 1
}
return d.fountain.Progress()
}
func (d *Decoder) Result() (string, []byte, error) {
if d.data != nil {
return d.typ, d.data, nil
}
v, err := d.fountain.Result()
if v == nil {
return "", nil, err
}
return d.typ, v, err
}
func (d *Decoder) Add(ur string) error {
ur = strings.ToLower(ur)
const prefix = "ur:"
if !strings.HasPrefix(ur, prefix) {
return errors.New("ur: missing ur: prefix")
}
ur = ur[len(prefix):]
parts := strings.SplitN(ur, "/", 3)
if len(parts) < 2 {
return errors.New("ur: incomplete UR")
}
typ := parts[0]
if d.typ != "" && d.typ != typ {
return errors.New("ur: incompatible fragment")
}
d.typ = typ
var seqAndLen string
var fragment string
if len(parts) == 2 {
fragment = parts[1]
} else {
seqAndLen, fragment = parts[1], parts[2]
}
enc, err := bytewords.Decode(fragment)
if err != nil {
return fmt.Errorf("ur: invalid fragment: %w", err)
}
if seqAndLen != "" {
var seq, n int
if _, err := fmt.Sscanf(seqAndLen, "%d-%d", &seq, &n); err != nil {
return fmt.Errorf("ur: invalid sequence %q", seqAndLen)
}
if err := d.fountain.Add(enc); err != nil {
return err
}
} else {
d.data = enc
}
return nil
}

119
bc/ur/ur_test.go Normal file
View File

@ -0,0 +1,119 @@
package ur
import (
"encoding/hex"
"reflect"
"strings"
"testing"
)
func TestDecode(t *testing.T) {
tests := []struct {
urs []string
wantType string
want string
seqLen int
seqNums []int
error bool
}{
{[]string{"r:crypto-seed/oyadgdiywlamaejszswdwytltifeenftlnmnwkbdhnssro"}, "", "", 0, nil, true},
{
[]string{"ur:crypto-seed/oyadgdiywlamaejszswdwytltifeenftlnmnwkbdhnssro"},
"crypto-seed", "a1015066e9060071faeaeed5d045363a868ef4",
1, []int{1},
false,
},
{
[]string{"ur:crypto-output/taadmetaadmtoeadadaolftaaddloxaxhdclaxsbsgptsolkltkndsmskiaelfhhmdimcnmnlgutzotecpsfveylgrbdhptbpsveosaahdcxhnganelacwldjnlschnyfxjyplrllfdrplpswdnbuyctlpwyfmmhgsgtwsrymtldamtaaddyoeadlaaxaeattaaddyoyadlnadwkaewklawktaaddloxaxhdclaoztnnhtwtpslgndfnwpzedrlomnclchrdfsayntlplplojznslfjejecpptlgbgwdaahdcxwtmhnyzmpkkbvdpyvwutglbeahmktyuogusnjonththhdwpsfzvdfpdlcndlkensamtaaddyoeadlfaewkaocyrycmrnvwattaaddyoyadlnaewkaewklawktdbsfttn"},
"crypto-output", "d90191d90196a201010282d9012fa403582103cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a704582060499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd968906d90130a20180030007d90130a1018601f400f480f4d9012fa403582102fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea045820f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c06d90130a2018200f4021abd16bee507d90130a1018600f400f480f4",
1, []int{1},
false,
},
{
[]string{
"UR:BYTES/1-3/LPADAXCFAODNCYDLMSDPONHDRHHKAODECNCXFWJZKPIHHGHSJZJZIHJYCXGTKPJZJYINJKINIOCXJKIHJYKPJOCXIYINJZIHBKCNCXJYISINJKCXIYINJZIHCXIAJLJTJYHSINJTJKCXJLJTJZKKCXJOKPIDJZINIACXJEIHKKJKCXHSJTIECXINJKCXJKHSIYIHCXJYJLBKCNCXIEINJKJYJPINIDKPJYIHCXHSJNJLJTIOCXIAJLJKINIOJTIHJPJKBKCNBKGLHSJNIHFTCXJKISBKGDJLJZINIAKKFTCXEYCXJLIYCXEOBKFYIHJPINKOHSJYINJLJTFTCXJNDLEEETDIDLDYDIDLDYDIDLEYDIBKFGJLJPJNHSJYFTCXGDEYHGGUFDBKLSTSKGSS",
"UR:BYTES/2-3/LPAOAXCFAODNCYDLMSDPONHDRHBKECFPDYETDYEEFEEOFTCXKSJOKPIDENFGEHEEETGSJTIMGOISFLJPFDIYFEGLENGDHSETHFJEKTFGETGSENFGGEJSHKFPGSKSFPJEKPFDIYHSIAIYHFISGTGSHFHKEEGTGMKPGOHFGTKSJPESJOIOKPFPKOENEMFYFDKSEHHKFGKSJSJLGRGLETJKEEGYIYHTJYFYESJKGMEYKSGMFXIYIYGHJSINESFEETFGINFGGSFPHKJEETBKBKFYFYEEFGFPFYFEFEFTCXKSJOKPIDENFYJTIHIEINGOKPHKETGDIAIAENFGIHIMETHKJYEYHTJTJYGDFXKKFGIEJOIDFDFWJEGLHFEMFEHSKTIHJKGMGTIDIAENVSAEGDBD",
"UR:BYTES/3-3/LPAXAXCFAODNCYDLMSDPONHDRHINESGTGRGRGTISGRFEKOEEGEGTGTKNKTFYGEIAJEHSHFEEIAKNFWKOGLIEIAENINJEKTGSINHTJSIEGOJSGTIEECHTGRGYFLHKHSGYGHEEIAHDGTIHHFIMIYBKBKESFWFPFXFYECFXDYFTCXKSJOKPIDENFEIHIYJPFXJPGTFPIEKPISGLKTJTJKFDIDEOIEFPJKETFYHKHTGUKTEEIYENEOHGKKGMENFYHSFEFWKKGOFDIMKTKOGDFYIEISIAKNIMEHECFGKKFWFWFLEEJYIDFEGEJYIYEEKOGMGRGHKOEHJTIOECGUGDGDJTHGKOEHGDKOIHEHIYEHECFEGEIYINFWHKECJLHKFYGLENHFGSFEFXBKBKHEMEBWOX",
},
"bytes", "5902282320426c756557616c6c6574204d756c74697369672073657475702066696c650a2320746869732066696c6520636f6e7461696e73206f6e6c79207075626c6963206b65797320616e64206973207361666520746f0a23206469737472696275746520616d6f6e6720636f7369676e6572730a230a4e616d653a2073680a506f6c6963793a2032206f6620330a44657269766174696f6e3a206d2f3438272f30272f30272f32270a466f726d61743a2050325753480a0a35413038303445333a207870756236463134384c6e6a556847724866454e36506138566b7746384c36464a7159414c78416b75486661636656684d4c5659344d527555564d7872397067754176363744487831594678716f4b4e38733451665a74443973523278524366665471693945384669464c41596b380a0a44443446414445453a207870756236446e656469557559385063633646656a385974325a6e745043794664706248426b4e56374561776573524d62633669394d4b4b4d684b4576344a4d4d7a77444a636b615634637a42764e646336696b774c695a716455714d64355a4b5147596151543463584d65566a660a0a39424143443543303a2078707562364565667243724d416475684e776e734862336441733844595a53773466363357795236446145427955486a777650446468637a6a31354679424247347462454a74663476524b5476316e67355350506e57763150766531663135454a66694259356f59444e36564c45430a0a",
3, []int{1, 2, 3},
false,
},
{[]string{"r:crypto-seed/oyadgdiywlamaejszswdwytltifeenftlnmnwkbdhnssro"}, "", "", 0, nil, true},
{
[]string{
"UR:CRYPTO-OUTPUT/1347-2/LPCFAHFXAOCFADIOCYCMSWIDBYHDQZCYHNOEDWSBMUAMWYOTAHPFFXNECKNBNTHKDEADHLVLJKLYCMAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYUTGWPMWYAXAAAYCYCPMTMUKTTAADDLOLAOWKAXHDCLAOZOJPGDLBSABTUYPTDTMEPAKEGRQZIYBWBKTAFTLOJTJKCHGDEORKFXVLRFKSHTJNAAHDCXMDQDGABWMULBONWNSWCXHPGMHPREKIVYGYKODAVTFELNREMDRNISVDBWIDTEWESKAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYNDPSTLRTAXAAAYCYMSWPETYTAEVDTLISPT",
"UR:CRYPTO-OUTPUT/1355-2/LPCFAHGRAOCFADIOCYCMSWIDBYHDQZSRHSEOYKSGAAOXWSOYATEONYNNEHAMNEPMDNHKKEVTTNROHHDRSRGLPDSRFRJSJEHFTOLGBAHLCFJTMHLUDWTEESVWJPTYPFMOTLHTJPJZPTRPCNURVTCMNLTPNTENGMATUYTBTIHPVEWTVTKKCEJKZOHEPLGHKIYLGSESMDKICLTPCMCMTETPBDDRJLJKBZGDECIDFWTECTKKTDKPEEPMCXHNQDRFBYIYKIRSPYTODKROGYHERYIODSWEMELGESFYPTBWMSGEJERSEYHNWZKGISSTLNURDIFSVSDMJPKOMTLABYBGTBTEFNBBYTJPKOCTPYIORDURLRASSKFMTTMKCNFLLNVWWPTSBAGWTTPYMUOELP",
},
"crypto-output",
"d90191d90197a201020283d9012fa602f403582103a9394a2f1a4f99613a716956c8540f6dba6f18931c2639107221b267d740af23045820dbe80cbb4e0e418b06f470d2afe7a8c17be701ab206c59a65e65a824016a6c7005d90131a20100020006d90130a301881830f500f500f502f5021a5a0804e30304081ac7bce7a8d9012fa602f4035821022196adc25fde169fe92e70769059102275d2b40cc98776eaab92b82a86135e92045820438eff7b3b36b6d11a60a22ccb9306eea305b0439f1ea09d5928015de373811605d90131a20100020006d90130a301881830f500f500f502f5021add4fadee0304081a22969377d9012fa602f403582102fb72507fc20ddba92991b17c4bb466130ad93a886e73175033bb43e3bc785a6d04582095b34913937fa5f1c6205b525bb57de1517625e04586b595be68e71362d3edc505d90131a20100020006d90130a301881830f500f500f502f5021a9bacd5c00304081a97ec38f9",
2, []int{1347, 1355},
false,
},
{
[]string{
"ur:bytes/1-9/lpadascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtdkgslpgh",
"ur:bytes/2-9/lpaoascfadaxcywenbpljkhdcagwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsgmghhkhstlrdcxaefz",
"ur:bytes/3-9/lpaxascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjksopdzmol",
"ur:bytes/4-9/lpaaascfadaxcywenbpljkhdcasotkhemthydawydtaxneurlkosgwcekonertkbrlwmplssjtammdplolsbrdzcrtas",
"ur:bytes/5-9/lpahascfadaxcywenbpljkhdcatbbdfmssrkzmcwnezelennjpfzbgmuktrhtejscktelgfpdlrkfyfwdajldejokbwf",
"ur:bytes/6-9/lpamascfadaxcywenbpljkhdcackjlhkhybssklbwefectpfnbbectrljectpavyrolkzczcpkmwidmwoxkilghdsowp",
"ur:bytes/7-9/lpatascfadaxcywenbpljkhdcavszmwnjkwtclrtvaynhpahrtoxmwvwatmedibkaegdosftvandiodagdhthtrlnnhy",
"ur:bytes/8-9/lpayascfadaxcywenbpljkhdcadmsponkkbbhgsoltjntegepmttmoonftnbuoiyrehfrtsabzsttorodklubbuyaetk",
"ur:bytes/9-9/lpasascfadaxcywenbpljkhdcajskecpmdckihdyhphfotjojtfmlnwmadspaxrkytbztpbauotbgtgtaeaevtgavtny",
"ur:bytes/10-9/lpbkascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtwdkiplzs",
"ur:bytes/11-9/lpbdascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjkvetiiapk",
"ur:bytes/12-9/lpbnascfadaxcywenbpljkhdcarllaluzmdmgstospeyiefmwejlwtpedamktksrvlcygmzemovovllarodtmtbnptrs",
"ur:bytes/13-9/lpbtascfadaxcywenbpljkhdcamtkgtpknghchchyketwsvwgwfdhpgmgtylctotzopdrpayoschcmhplffziachrfgd",
"ur:bytes/14-9/lpbaascfadaxcywenbpljkhdcapazewnvonnvdnsbyleynwtnsjkjndeoldydkbkdslgjkbbkortbelomueekgvstegt",
"ur:bytes/15-9/lpbsascfadaxcywenbpljkhdcaynmhpddpzmversbdqdfyrehnqzlugmjzmnmtwmrouohtstgsbsahpawkditkckynwt",
"ur:bytes/16-9/lpbeascfadaxcywenbpljkhdcawygekobamwtlihsnpalnsghenskkiynthdzotsimtojetprsttmukirlrsbtamjtpd",
"ur:bytes/17-9/lpbyascfadaxcywenbpljkhdcamklgftaxykpewyrtqzhydntpnytyisincxmhtbceaykolduortotiaiaiafhiaoyce",
"ur:bytes/18-9/lpbgascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtntwkbkwy",
"ur:bytes/19-9/lpbwascfadaxcywenbpljkhdcadekicpaajootjzpsdrbalpeywllbdsnbinaerkurspbncxgslgftvtsrjtksplcpeo",
"ur:bytes/20-9/lpbbascfadaxcywenbpljkhdcayapmrleeleaxpasfrtrdkncffwjyjzgyetdmlewtkpktgllepfrltataztksmhkbot",
},
"bytes", "590100916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3ccba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f965e25ee29039fdf8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3ec4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f595e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff173f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d",
9, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
false,
},
}
for _, test := range tests {
var d Decoder
for _, ur := range test.urs {
if err := d.Add(strings.ToLower(ur)); err != nil {
if !test.error {
t.Error(err)
}
} else {
if test.error {
t.Errorf("%q unexpectedly decoded successfully", ur)
}
}
}
if test.error {
continue
}
typ, got, err := d.Result()
if err != nil {
t.Fatal(err)
}
if typ != test.wantType {
t.Errorf("%q: decoded type %q, wanted %q", test.urs[0], typ, test.wantType)
}
want, err := hex.DecodeString(test.want)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("%q: decoded to %x; wanted %x", test.urs[0], got, want)
}
for i, seqNum := range test.seqNums {
got := Encode(test.wantType, want, seqNum, test.seqLen)
want := strings.ToLower(test.urs[i])
if want != got {
t.Errorf("seqNum %d of %s is %s expected %s", seqNum, test.want, got, want)
}
}
}
}

640
bc/urtypes/urtypes.go Normal file
View File

@ -0,0 +1,640 @@
// Package urtypes implements decoders for UR types specified in [BCR-2020-006].
//
// [BCR-2020-006]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-006-urtypes.md
package urtypes
import (
"encoding/binary"
"errors"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/fxamacker/cbor/v2"
)
type OutputDescriptor struct {
Title string
Script Script
Threshold int
Type MultisigType
Keys []KeyDescriptor
}
type KeyDescriptor struct {
Network *chaincfg.Params
MasterFingerprint uint32
DerivationPath Path
Children []Derivation
KeyData []byte
ChainCode []byte
ParentFingerprint uint32
}
type Derivation struct {
Type DerivationType
// Index is the child index, without the hardening offset.
// For RangeDerivations, Index is the start of the range.
Index uint32
Hardened bool
// End represents the end of a RangeDerivation.
End uint32
}
type DerivationType int
const (
ChildDerivation DerivationType = iota
WildcardDerivation
RangeDerivation
)
type Script int
const (
UnknownScript Script = iota
P2SH
P2SH_P2WSH
P2SH_P2WPKH
P2PKH
P2WSH
P2WPKH
P2TR
)
func (s Script) String() string {
switch s {
case P2SH:
return "Legacy (P2SH)"
case P2SH_P2WSH:
return "Nested Segwit (P2SH-P2WSH)"
case P2SH_P2WPKH:
return "Nested Segwit (P2SH-P2WPKH)"
case P2PKH:
return "Legacy (P2PKH)"
case P2WSH:
return "Segwit (P2WSH)"
case P2WPKH:
return "Segwit (P2WPKH)"
case P2TR:
return "Taproot (P2TR)"
default:
return "Unknown"
}
}
type MultisigType int
const (
Singlesig MultisigType = iota
SortedMulti
)
// Singlesig reports whether the script is for single-sig.
func (s Script) Singlesig() bool {
for _, s2 := range []Script{P2PKH, P2WPKH, P2SH_P2WPKH, P2TR} {
if s == s2 {
return true
}
}
return false
}
// DerivationPath returns the standard derivation path
// for the script. It panics if the script is unknown.
func (s Script) DerivationPath() Path {
switch s {
case P2WPKH:
return Path{
hdkeychain.HardenedKeyStart + 84,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 0,
}
case P2PKH:
return Path{
hdkeychain.HardenedKeyStart + 44,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 0,
}
case P2SH_P2WPKH:
return Path{
hdkeychain.HardenedKeyStart + 49,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 0,
}
case P2TR:
return Path{
hdkeychain.HardenedKeyStart + 86,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 0,
}
case P2SH:
return Path{
hdkeychain.HardenedKeyStart + 45,
}
case P2SH_P2WSH:
return Path{
hdkeychain.HardenedKeyStart + 48,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 1,
}
case P2WSH:
return Path{
hdkeychain.HardenedKeyStart + 48,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 0,
hdkeychain.HardenedKeyStart + 2,
}
}
panic("unknown script")
}
// Encode the output descriptor in the format described by
// [BCR-2020-010].
//
// [BCR-2020-010]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-010-output-desc.md
func (o OutputDescriptor) Encode() []byte {
var v any
switch o.Type {
case SortedMulti:
m := struct {
Threshold int `cbor:"1,keyasint,omitempty"`
Keys []cbor.Tag `cbor:"2,keyasint"`
}{
Threshold: o.Threshold,
}
for _, k := range o.Keys {
m.Keys = append(m.Keys, cbor.Tag{
Number: tagHDKey,
Content: k.toCBOR(),
})
}
v = cbor.Tag{
Number: uint64(tagSortedMulti),
Content: m,
}
case Singlesig:
v = cbor.Tag{
Number: tagHDKey,
Content: o.Keys[0].toCBOR(),
}
default:
panic("invalid type")
}
var tags []uint64
switch o.Script {
case P2SH:
tags = []uint64{tagSH}
case P2SH_P2WSH:
tags = []uint64{tagSH, tagWSH}
case P2SH_P2WPKH:
tags = []uint64{tagSH, tagWPKH}
case P2PKH:
tags = []uint64{tagP2PKH}
case P2WSH:
tags = []uint64{tagWSH}
case P2WPKH:
tags = []uint64{tagWPKH}
case P2TR:
tags = []uint64{tagTR}
default:
panic("invalid type")
}
for i := len(tags) - 1; i >= 0; i-- {
v = cbor.Tag{
Number: tags[i],
Content: v,
}
}
enc, err := encMode.Marshal(v)
if err != nil {
panic(err)
}
return enc
}
func (k KeyDescriptor) ExtendedKey() *hdkeychain.ExtendedKey {
var fp [4]byte
binary.BigEndian.PutUint32(fp[:], k.ParentFingerprint)
childNum := uint32(0)
if len(k.DerivationPath) > 0 {
childNum = k.DerivationPath[len(k.DerivationPath)-1]
}
return hdkeychain.NewExtendedKey(
k.Network.HDPublicKeyID[:],
k.KeyData, k.ChainCode, fp[:], uint8(len(k.DerivationPath)),
childNum, false,
)
}
func (k KeyDescriptor) String() string {
return k.ExtendedKey().String()
}
// Encode the key in the format described by [BCR-2020-007].
//
// [BCR-2020-007]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-007-hdkey.md
func (k KeyDescriptor) toCBOR() hdKey {
var children []any
for _, c := range k.Children {
switch c.Type {
case ChildDerivation:
children = append(children, c.Index, c.Hardened)
case RangeDerivation:
children = append(children, c.Index, c.End, c.Hardened)
case WildcardDerivation:
children = append(children, []any{}, c.Hardened)
}
}
depth := len(k.DerivationPath)
if depth == len(k.DerivationPath) {
// No need to store the depth if the derivation path is present.
depth = 0
}
network := mainnet
if k.Network == &chaincfg.TestNet3Params {
network = testnet
}
return hdKey{
UseInfo: useInfo{
Network: network,
},
KeyData: k.KeyData,
ChainCode: k.ChainCode,
ParentFingerprint: k.ParentFingerprint,
Origin: keyPath{
Fingerprint: k.MasterFingerprint,
Depth: uint8(depth),
Components: k.DerivationPath.components(),
},
Children: keyPath{
Components: children,
},
}
}
// Encode the key in the format described by [BCR-2020-007].
//
// [BCR-2020-007]: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-007-hdkey.md
func (k KeyDescriptor) Encode() []byte {
b, err := encMode.Marshal(k.toCBOR())
if err != nil {
// Always valid by construction.
panic(err)
}
return b
}
type Path []uint32
func (p Path) components() []any {
var comp []any
for _, c := range p {
hard := c >= hdkeychain.HardenedKeyStart
if hard {
c -= hdkeychain.HardenedKeyStart
}
comp = append(comp, c, hard)
}
return comp
}
func (p Path) String() string {
var d strings.Builder
d.WriteRune('m')
for _, p := range p {
d.WriteByte('/')
idx := p
if p >= hdkeychain.HardenedKeyStart {
idx -= hdkeychain.HardenedKeyStart
}
d.WriteString(strconv.Itoa(int(idx)))
if p >= hdkeychain.HardenedKeyStart {
d.WriteRune('h')
}
}
return d.String()
}
type seed struct {
Payload []byte `cbor:"1,keyasint"`
}
type multi struct {
Threshold int `cbor:"1,keyasint"`
Keys []cbor.RawMessage `cbor:"2,keyasint"`
}
// account is the CBOR representation of a crypto-account.
type account struct {
MasterFingerprint uint32 `cbor:"1,keyasint,omitempty"`
OutputDescriptors []cbor.RawMessage `cbor:"2,keyasint,omitempty"`
}
// hdKey is the CBOR representation of a crypto-hdkey.
type hdKey struct {
IsMaster bool `cbor:"1,keyasint,omitempty"`
IsPrivate bool `cbor:"2,keyasint,omitempty"`
KeyData []byte `cbor:"3,keyasint"`
ChainCode []byte `cbor:"4,keyasint,omitempty"`
UseInfo useInfo `cbor:"5,keyasint,omitempty"`
Origin keyPath `cbor:"6,keyasint,omitempty"`
Children keyPath `cbor:"7,keyasint,omitempty"`
ParentFingerprint uint32 `cbor:"8,keyasint,omitempty"`
}
type useInfo struct {
Type uint32 `cbor:"1,keyasint,omitempty"`
Network int `cbor:"2,keyasint,omitempty"`
}
type keyPath struct {
Components []any `cbor:"1,keyasint,omitempty"`
Fingerprint uint32 `cbor:"2,keyasint,omitempty"`
Depth uint8 `cbor:"3,keyasint,omitempty"`
}
const (
tagHDKey = 303
tagKeyPath = 304
tagUseInfo = 305
tagSH = 400
tagWSH = 401
tagP2PKH = 403
tagWPKH = 404
tagTR = 409
tagMulti = 406
tagSortedMulti = 407
)
var encMode cbor.EncMode
var decMode cbor.DecMode
func init() {
tags := cbor.NewTagSet()
if err := tags.Add(cbor.TagOptions{DecTag: cbor.DecTagOptional}, reflect.TypeOf(hdKey{}), tagHDKey); err != nil {
panic(err)
}
if err := tags.Add(cbor.TagOptions{DecTag: cbor.DecTagOptional, EncTag: cbor.EncTagRequired}, reflect.TypeOf(keyPath{}), tagKeyPath); err != nil {
panic(err)
}
if err := tags.Add(cbor.TagOptions{DecTag: cbor.DecTagOptional, EncTag: cbor.EncTagRequired}, reflect.TypeOf(useInfo{}), tagUseInfo); err != nil {
panic(err)
}
em, err := cbor.CoreDetEncOptions().EncModeWithTags(tags)
if err != nil {
panic(err)
}
encMode = em
dm, err := cbor.DecOptions{}.DecModeWithTags(tags)
if err != nil {
panic(err)
}
decMode = dm
}
func Parse(typ string, enc []byte) (any, error) {
switch typ {
case "crypto-seed":
var s seed
if err := decMode.Unmarshal(enc, &s); err != nil {
return nil, fmt.Errorf("ur: %s: %w", typ, err)
}
return s, nil
case "crypto-account":
// Limited support for crypto-account: unpack a single output
// descriptor and treat it like crypto-output.
var acc account
if err := decMode.Unmarshal(enc, &acc); err != nil {
return nil, fmt.Errorf("ur: crypto-account: %w", err)
}
if len(acc.OutputDescriptors) != 1 {
return nil, fmt.Errorf("ur: crypto-account: zero or multiple crypto-outputs")
}
enc = acc.OutputDescriptors[0]
desc, err := parseOutputDescriptor(decMode, acc.OutputDescriptors[0])
if err != nil {
return nil, fmt.Errorf("ur: crypto-account: %w", err)
}
if !desc.Script.Singlesig() {
return nil, fmt.Errorf("ur: crypto-account: invalid single-sig script: %s", desc.Script)
}
return desc, nil
case "crypto-output":
desc, err := parseOutputDescriptor(decMode, enc)
if err != nil {
return nil, fmt.Errorf("ur: crypto-output: %w", err)
}
return desc, nil
case "crypto-hdkey":
key, err := parseHDKey(enc)
if err != nil {
return nil, fmt.Errorf("ur: crypto-hdkey: %w", err)
}
return key, nil
case "bytes":
var content []byte
if err := decMode.Unmarshal(enc, &content); err != nil {
return nil, fmt.Errorf("ur: bytes decoding failed: %w", err)
}
return content, nil
default:
return nil, fmt.Errorf("ur: unknown type %q", typ)
}
}
const mainnet = 0
const testnet = 1
func parseHDKey(enc []byte) (KeyDescriptor, error) {
var k hdKey
if err := decMode.Unmarshal(enc, &k); err != nil {
return KeyDescriptor{}, fmt.Errorf("ur: crypto-hdkey decoding failed: %w", err)
}
const cointypeBTC = 0
if k.UseInfo.Type != cointypeBTC {
return KeyDescriptor{}, fmt.Errorf("ur: crypto-hdkey key has unsupported coin type %d", k.UseInfo.Type)
}
children, err := parseKeypath(k.Children.Components)
if err != nil {
return KeyDescriptor{}, err
}
if len(k.KeyData) != 33 {
return KeyDescriptor{}, fmt.Errorf("ur: crypto-hdkey key is %d bytes, expected 33", len(k.KeyData))
}
if len(k.ChainCode) != 32 {
return KeyDescriptor{}, fmt.Errorf("ur: crypto-hdkey chain code is %d bytes, expected 32", len(k.ChainCode))
}
var net *chaincfg.Params
switch n := k.UseInfo.Network; n {
case mainnet:
net = &chaincfg.MainNetParams
case testnet:
net = &chaincfg.TestNet3Params
default:
return KeyDescriptor{}, fmt.Errorf("ur: unknown coininfo network %d", n)
}
comps, err := parseKeypath(k.Origin.Components)
if err != nil {
return KeyDescriptor{}, err
}
var devPath Path
for _, d := range comps {
if d.Type != ChildDerivation {
return KeyDescriptor{}, fmt.Errorf("ur: wildcards or ranges not allowed in origin path")
}
idx := d.Index
if d.Hardened {
idx += hdkeychain.HardenedKeyStart
}
devPath = append(devPath, idx)
}
depth := k.Origin.Depth
if depth != 0 && int(depth) != len(devPath) {
return KeyDescriptor{}, fmt.Errorf("ur: origin depth is %d but expected %d", depth, len(devPath))
}
return KeyDescriptor{
Network: net,
MasterFingerprint: k.Origin.Fingerprint,
DerivationPath: devPath,
Children: children,
KeyData: k.KeyData,
ChainCode: k.ChainCode,
ParentFingerprint: k.ParentFingerprint,
}, nil
}
func parseOutputDescriptor(mode cbor.DecMode, enc []byte) (OutputDescriptor, error) {
var tags []uint64
for {
var raw cbor.RawTag
if err := mode.Unmarshal(enc, &raw); err != nil {
break
}
tags = append(tags, raw.Number)
enc = raw.Content
}
if len(tags) == 0 {
return OutputDescriptor{}, errors.New("ur: missing descriptor tag")
}
var desc OutputDescriptor
first := tags[0]
tags = tags[1:]
switch first {
case tagSH:
desc.Script = P2SH
if len(tags) == 0 {
break
}
switch tags[0] {
case tagWSH:
desc.Script = P2SH_P2WSH
tags = tags[1:]
case tagWPKH:
desc.Script = P2SH_P2WPKH
tags = tags[1:]
}
case tagP2PKH:
desc.Script = P2PKH
case tagTR:
desc.Script = P2TR
case tagWSH:
desc.Script = P2WSH
case tagWPKH:
desc.Script = P2WPKH
default:
return OutputDescriptor{}, fmt.Errorf("ur: unknown script type tag: %d", first)
}
if len(tags) == 0 {
return OutputDescriptor{}, errors.New("ur: missing descriptor script tag")
}
funcNumber := tags[0]
tags = tags[1:]
if len(tags) > 0 {
return OutputDescriptor{}, errors.New("ur: extra tags")
}
switch funcNumber {
case tagHDKey: // singlesig
desc.Type = Singlesig
k, err := parseHDKey(enc)
if err != nil {
return OutputDescriptor{}, err
}
desc.Threshold = 1
desc.Keys = append(desc.Keys, k)
case tagSortedMulti:
desc.Type = SortedMulti
var m multi
if err := mode.Unmarshal(enc, &m); err != nil {
return OutputDescriptor{}, err
}
desc.Threshold = m.Threshold
for _, k := range m.Keys {
keyDesc, err := parseHDKey([]byte(k))
if err != nil {
return OutputDescriptor{}, err
}
desc.Keys = append(desc.Keys, keyDesc)
}
default:
return desc, fmt.Errorf("unknown script function tag: %d", funcNumber)
}
return desc, nil
}
func parseKeypath(comp []any) ([]Derivation, error) {
if len(comp)%2 == 1 {
return nil, errors.New("odd number of components")
}
var path []Derivation
for i := 0; i < len(comp); i += 2 {
d, h := comp[i], comp[i+1]
var deriv Derivation
switch d := d.(type) {
case uint64:
if d > math.MaxUint32 {
return nil, errors.New("child index out of range")
}
deriv = Derivation{
Type: ChildDerivation,
Index: uint32(d),
}
case []any:
switch len(d) {
case 0:
deriv = Derivation{
Type: WildcardDerivation,
}
case 2:
start, ok1 := d[0].(uint64)
end, ok2 := d[1].(uint64)
if !ok1 || !ok2 || start > math.MaxUint32 || end > math.MaxUint32 {
return nil, errors.New("invalid range derivation")
}
deriv = Derivation{
Type: RangeDerivation,
Index: uint32(start),
End: uint32(end),
}
default:
return nil, errors.New("invalid wildcard derivation")
}
default:
return nil, errors.New("unknown component type")
}
hardened, ok := h.(bool)
if !ok {
return nil, errors.New("invalid hardened flag")
}
deriv.Hardened = hardened
path = append(path, deriv)
}
return path, nil
}

323
bc/urtypes/urtypes_test.go Normal file
View File

@ -0,0 +1,323 @@
package urtypes
import (
"encoding/hex"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
)
func TestDecode(t *testing.T) {
tests := []struct {
_type string
enc string
want any
}{
{
"crypto-seed",
"a1015066e9060071faeaeed5d045363a868ef4",
seed{Payload: []byte{102, 233, 6, 0, 113, 250, 234, 238, 213, 208, 69, 54, 58, 134, 142, 244}},
},
}
for _, test := range tests {
enc, err := hex.DecodeString(test.enc)
if err != nil {
t.Fatal(err)
}
got, err := Parse(test._type, enc)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("%s decoded to\n%#v\nwanted\n%#v", test.enc, got, test.want)
}
}
}
func TestOutputDescriptor(t *testing.T) {
twoOfThree := OutputDescriptor{
Script: P2WSH,
Threshold: 2,
Type: SortedMulti,
Keys: []KeyDescriptor{
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0xdd4fadee,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 48, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart + 2},
KeyData: []byte{0x2, 0x21, 0x96, 0xad, 0xc2, 0x5f, 0xde, 0x16, 0x9f, 0xe9, 0x2e, 0x70, 0x76, 0x90, 0x59, 0x10, 0x22, 0x75, 0xd2, 0xb4, 0xc, 0xc9, 0x87, 0x76, 0xea, 0xab, 0x92, 0xb8, 0x2a, 0x86, 0x13, 0x5e, 0x92},
ChainCode: []byte{0x43, 0x8e, 0xff, 0x7b, 0x3b, 0x36, 0xb6, 0xd1, 0x1a, 0x60, 0xa2, 0x2c, 0xcb, 0x93, 0x6, 0xee, 0xa3, 0x5, 0xb0, 0x43, 0x9f, 0x1e, 0xa0, 0x9d, 0x59, 0x28, 0x1, 0x5d, 0xe3, 0x73, 0x81, 0x16},
ParentFingerprint: 0x22969377,
},
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0x9bacd5c0,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 48, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart + 2},
KeyData: []byte{0x2, 0xfb, 0x72, 0x50, 0x7f, 0xc2, 0xd, 0xdb, 0xa9, 0x29, 0x91, 0xb1, 0x7c, 0x4b, 0xb4, 0x66, 0x13, 0xa, 0xd9, 0x3a, 0x88, 0x6e, 0x73, 0x17, 0x50, 0x33, 0xbb, 0x43, 0xe3, 0xbc, 0x78, 0x5a, 0x6d},
ChainCode: []byte{0x95, 0xb3, 0x49, 0x13, 0x93, 0x7f, 0xa5, 0xf1, 0xc6, 0x20, 0x5b, 0x52, 0x5b, 0xb5, 0x7d, 0xe1, 0x51, 0x76, 0x25, 0xe0, 0x45, 0x86, 0xb5, 0x95, 0xbe, 0x68, 0xe7, 0x13, 0x62, 0xd3, 0xed, 0xc5},
ParentFingerprint: 0x97ec38f9,
},
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0x5a0804e3,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 48, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart + 2},
KeyData: []byte{0x3, 0xa9, 0x39, 0x4a, 0x2f, 0x1a, 0x4f, 0x99, 0x61, 0x3a, 0x71, 0x69, 0x56, 0xc8, 0x54, 0xf, 0x6d, 0xba, 0x6f, 0x18, 0x93, 0x1c, 0x26, 0x39, 0x10, 0x72, 0x21, 0xb2, 0x67, 0xd7, 0x40, 0xaf, 0x23},
ChainCode: []byte{0xdb, 0xe8, 0xc, 0xbb, 0x4e, 0xe, 0x41, 0x8b, 0x6, 0xf4, 0x70, 0xd2, 0xaf, 0xe7, 0xa8, 0xc1, 0x7b, 0xe7, 0x1, 0xab, 0x20, 0x6c, 0x59, 0xa6, 0x5e, 0x65, 0xa8, 0x24, 0x1, 0x6a, 0x6c, 0x70},
ParentFingerprint: 0xc7bce7a8,
},
},
}
tests := []struct {
desc OutputDescriptor
want string
}{
{
OutputDescriptor{
Script: P2WSH,
Threshold: 1,
Type: SortedMulti,
Keys: []KeyDescriptor{
{
Network: &chaincfg.MainNetParams,
Children: []Derivation{
{Index: 1},
{Index: 0},
{Type: WildcardDerivation},
},
KeyData: []byte{0x3, 0xcb, 0xca, 0xa9, 0xc9, 0x8c, 0x87, 0x7a, 0x26, 0x97, 0x7d, 0x0, 0x82, 0x5c, 0x95, 0x6a, 0x23, 0x8e, 0x8d, 0xdd, 0xfb, 0xd3, 0x22, 0xcc, 0xe4, 0xf7, 0x4b, 0xb, 0x5b, 0xd6, 0xac, 0xe4, 0xa7},
ChainCode: []byte{0x60, 0x49, 0x9f, 0x80, 0x1b, 0x89, 0x6d, 0x83, 0x17, 0x9a, 0x43, 0x74, 0xae, 0xb7, 0x82, 0x2a, 0xae, 0xac, 0xea, 0xa0, 0xdb, 0x1f, 0x85, 0xee, 0x3e, 0x90, 0x4c, 0x4d, 0xef, 0xbd, 0x96, 0x89},
ParentFingerprint: 0x00000000,
},
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0xbd16bee5,
DerivationPath: Path{0},
Children: []Derivation{
{Index: 0},
{Index: 0},
{Type: WildcardDerivation},
},
KeyData: []byte{0x2, 0xfc, 0x9e, 0x5a, 0xf0, 0xac, 0x8d, 0x9b, 0x3c, 0xec, 0xfe, 0x2a, 0x88, 0x8e, 0x21, 0x17, 0xba, 0x3d, 0x8, 0x9d, 0x85, 0x85, 0x88, 0x6c, 0x9c, 0x82, 0x6b, 0x6b, 0x22, 0xa9, 0x8d, 0x12, 0xea},
ChainCode: []byte{0xf0, 0x90, 0x9a, 0xff, 0xaa, 0x7e, 0xe7, 0xab, 0xe5, 0xdd, 0x4e, 0x10, 0x5, 0x98, 0xd4, 0xdc, 0x53, 0xcd, 0x70, 0x9d, 0x5a, 0x5c, 0x2c, 0xac, 0x40, 0xe7, 0x41, 0x2f, 0x23, 0x2f, 0x7c, 0x9c},
ParentFingerprint: 0x00000000,
},
},
},
"d90191d90197a201010282d9012fa303582103cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a704582060499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd968907d90130a1018601f400f480f4d9012fa403582102fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea045820f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c06d90130a2018200f4021abd16bee507d90130a1018600f400f480f4",
},
{
twoOfThree,
"d90191d90197a201020283d9012fa4035821022196adc25fde169fe92e70769059102275d2b40cc98776eaab92b82a86135e92045820438eff7b3b36b6d11a60a22ccb9306eea305b0439f1ea09d5928015de373811606d90130a201881830f500f500f502f5021add4fadee081a22969377d9012fa403582102fb72507fc20ddba92991b17c4bb466130ad93a886e73175033bb43e3bc785a6d04582095b34913937fa5f1c6205b525bb57de1517625e04586b595be68e71362d3edc506d90130a201881830f500f500f502f5021a9bacd5c0081a97ec38f9d9012fa403582103a9394a2f1a4f99613a716956c8540f6dba6f18931c2639107221b267d740af23045820dbe80cbb4e0e418b06f470d2afe7a8c17be701ab206c59a65e65a824016a6c7006d90130a201881830f500f500f502f5021a5a0804e3081ac7bce7a8",
},
{
OutputDescriptor{
Script: P2WPKH, Threshold: 1, Keys: []KeyDescriptor{
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0x9c43e6c2,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 84, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart},
KeyData: []uint8{0x3, 0x3e, 0xd5, 0x1b, 0xcf, 0xf9, 0x30, 0xc6, 0x14, 0xe8, 0x61, 0xbf, 0xed, 0xff, 0x57, 0x69, 0x9b, 0x67, 0x8, 0x5a, 0x9f, 0x19, 0x77, 0x75, 0xbc, 0xc5, 0x41, 0xa9, 0xeb, 0xe8, 0x26, 0x8d, 0xe9},
ChainCode: []uint8{0x21, 0x23, 0x99, 0xa8, 0xdb, 0x12, 0x5c, 0x85, 0xf9, 0x41, 0xea, 0x12, 0x23, 0x1d, 0x8b, 0x5c, 0x7a, 0x76, 0xb8, 0x3e, 0x1, 0xd0, 0x3d, 0x16, 0xc5, 0x39, 0x58, 0xc5, 0x18, 0x28, 0x4f, 0x45},
ParentFingerprint: 0xd1e5a62d,
},
},
},
"d90194d9012fa4035821033ed51bcff930c614e861bfedff57699b67085a9f197775bcc541a9ebe8268de9045820212399a8db125c85f941ea12231d8b5c7a76b83e01d03d16c53958c518284f4506d90130a201861854f500f500f5021a9c43e6c2081ad1e5a62d",
},
{
OutputDescriptor{
Script: P2SH_P2WPKH, Threshold: 1, Keys: []KeyDescriptor{
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0x9866232b,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 49, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart},
KeyData: []uint8{0x2, 0xb1, 0x1d, 0x60, 0xe0, 0x23, 0x9, 0xc4, 0x80, 0xbb, 0xa1, 0x37, 0x77, 0x1a, 0xd6, 0x14, 0x62, 0x6b, 0xea, 0xf2, 0xef, 0x74, 0x34, 0x4e, 0xd1, 0xf, 0xd8, 0x3b, 0xb6, 0x3f, 0xeb, 0xcf, 0xa7},
ChainCode: []uint8{0x65, 0x8c, 0xa1, 0x47, 0x4, 0xcc, 0x49, 0xcb, 0x6, 0x64, 0x9d, 0x1b, 0xdd, 0x74, 0x6b, 0x11, 0x9f, 0x18, 0x5b, 0xf7, 0x7c, 0x1c, 0x48, 0x30, 0x73, 0xbb, 0x81, 0xe3, 0x35, 0x5a, 0xbc, 0x51},
ParentFingerprint: 0xe986734b,
},
},
},
"d90190d90194d9012fa403582102b11d60e02309c480bba137771ad614626beaf2ef74344ed10fd83bb63febcfa7045820658ca14704cc49cb06649d1bdd746b119f185bf77c1c483073bb81e3355abc5106d90130a201861831f500f500f5021a9866232b081ae986734b",
},
{
OutputDescriptor{
Script: P2PKH, Threshold: 1, Keys: []KeyDescriptor{
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0x9866232b,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 44, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart},
KeyData: []uint8{0x2, 0x72, 0x62, 0x46, 0x42, 0x95, 0xd, 0x14, 0x75, 0xf1, 0x6e, 0x46, 0xcc, 0x8d, 0x2b, 0x75, 0xcc, 0x2d, 0xe1, 0x2d, 0xf2, 0x9f, 0x29, 0xcf, 0x36, 0x97, 0x75, 0xb9, 0x5f, 0x66, 0xd2, 0x8e, 0x28},
ChainCode: []uint8{0xab, 0x20, 0x95, 0x8c, 0x7e, 0x9e, 0xd9, 0x9c, 0x91, 0x5d, 0x2c, 0x98, 0x7, 0x37, 0xf3, 0x12, 0x38, 0xd3, 0xb5, 0xab, 0x32, 0xb8, 0x8b, 0xda, 0xaa, 0x61, 0x91, 0x5b, 0xb5, 0xb3, 0xb4, 0xa4},
ParentFingerprint: 0xb62041ef,
},
},
},
"d90193d9012fa40358210272624642950d1475f16e46cc8d2b75cc2de12df29f29cf369775b95f66d28e28045820ab20958c7e9ed99c915d2c980737f31238d3b5ab32b88bdaaa61915bb5b3b4a406d90130a20186182cf500f500f5021a9866232b081ab62041ef",
},
{
OutputDescriptor{
Script: P2TR, Threshold: 1, Keys: []KeyDescriptor{
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0x9866232b,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 86, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart},
KeyData: []uint8{0x3, 0xd, 0x9f, 0x35, 0x47, 0x53, 0x4d, 0xd3, 0x32, 0x85, 0x56, 0x11, 0xaf, 0x48, 0xae, 0x34, 0x62, 0x25, 0xb0, 0xd4, 0xe1, 0xe5, 0xf8, 0x10, 0x57, 0xaa, 0x9e, 0x4c, 0x20, 0x58, 0x94, 0x87, 0xc5},
ChainCode: []uint8{0xc1, 0xaa, 0x32, 0xa1, 0x3d, 0x12, 0xcf, 0x59, 0x52, 0x8b, 0x58, 0x1e, 0x9b, 0x5d, 0x7, 0x4, 0x68, 0x57, 0x2e, 0x20, 0xf, 0x26, 0x4, 0x76, 0xa2, 0xee, 0xb2, 0x3a, 0xdc, 0x48, 0x4a, 0x43},
ParentFingerprint: 0x7fef547a,
},
},
},
"d90199d9012fa4035821030d9f3547534dd332855611af48ae346225b0d4e1e5f81057aa9e4c20589487c5045820c1aa32a13d12cf59528b581e9b5d070468572e200f260476a2eeb23adc484a4306d90130a201861856f500f500f5021a9866232b081a7fef547a",
},
{
OutputDescriptor{
Script: P2TR, Threshold: 1, Keys: []KeyDescriptor{
{
Network: &chaincfg.TestNet3Params,
KeyData: []uint8{0x3, 0xd, 0x9f, 0x35, 0x47, 0x53, 0x4d, 0xd3, 0x32, 0x85, 0x56, 0x11, 0xaf, 0x48, 0xae, 0x34, 0x62, 0x25, 0xb0, 0xd4, 0xe1, 0xe5, 0xf8, 0x10, 0x57, 0xaa, 0x9e, 0x4c, 0x20, 0x58, 0x94, 0x87, 0xc5},
ChainCode: []uint8{0xc1, 0xaa, 0x32, 0xa1, 0x3d, 0x12, 0xcf, 0x59, 0x52, 0x8b, 0x58, 0x1e, 0x9b, 0x5d, 0x7, 0x4, 0x68, 0x57, 0x2e, 0x20, 0xf, 0x26, 0x4, 0x76, 0xa2, 0xee, 0xb2, 0x3a, 0xdc, 0x48, 0x4a, 0x43},
ParentFingerprint: 0x7fef547a,
},
},
},
"d90199d9012fa4035821030d9f3547534dd332855611af48ae346225b0d4e1e5f81057aa9e4c20589487c5045820c1aa32a13d12cf59528b581e9b5d070468572e200f260476a2eeb23adc484a4305d90131a10201081a7fef547a",
},
}
for _, test := range tests {
got := test.desc.Encode()
gotHex := hex.EncodeToString(got)
if gotHex != test.want {
t.Errorf("%+v\nencoded to:%s\nwanted: %s\n", test.desc, gotHex, test.want)
}
parsed, err := Parse("crypto-output", got)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(parsed, test.desc) {
t.Errorf("descriptor:\n%+v\nroundtripped to\n%+v\n", test.desc, parsed)
}
}
}
func TestBytes(t *testing.T) {
tests := []struct {
enc string
want string
}{
{
"5902282320426c756557616c6c6574204d756c74697369672073657475702066696c650a2320746869732066696c6520636f6e7461696e73206f6e6c79207075626c6963206b65797320616e64206973207361666520746f0a23206469737472696275746520616d6f6e6720636f7369676e6572730a230a4e616d653a2073680a506f6c6963793a2032206f6620330a44657269766174696f6e3a206d2f3438272f30272f30272f32270a466f726d61743a2050325753480a0a35413038303445333a207870756236463134384c6e6a556847724866454e36506138566b7746384c36464a7159414c78416b75486661636656684d4c5659344d527555564d7872397067754176363744487831594678716f4b4e38733451665a74443973523278524366665471693945384669464c41596b380a0a44443446414445453a207870756236446e656469557559385063633646656a385974325a6e745043794664706248426b4e56374561776573524d62633669394d4b4b4d684b4576344a4d4d7a77444a636b615634637a42764e646336696b774c695a716455714d64355a4b5147596151543463584d65566a660a0a39424143443543303a2078707562364565667243724d416475684e776e734862336441733844595a53773466363357795236446145427955486a777650446468637a6a31354679424247347462454a74663476524b5476316e67355350506e57763150766531663135454a66694259356f59444e36564c45430a0a",
"2320426c756557616c6c6574204d756c74697369672073657475702066696c650a2320746869732066696c6520636f6e7461696e73206f6e6c79207075626c6963206b65797320616e64206973207361666520746f0a23206469737472696275746520616d6f6e6720636f7369676e6572730a230a4e616d653a2073680a506f6c6963793a2032206f6620330a44657269766174696f6e3a206d2f3438272f30272f30272f32270a466f726d61743a2050325753480a0a35413038303445333a207870756236463134384c6e6a556847724866454e36506138566b7746384c36464a7159414c78416b75486661636656684d4c5659344d527555564d7872397067754176363744487831594678716f4b4e38733451665a74443973523278524366665471693945384669464c41596b380a0a44443446414445453a207870756236446e656469557559385063633646656a385974325a6e745043794664706248426b4e56374561776573524d62633669394d4b4b4d684b4576344a4d4d7a77444a636b615634637a42764e646336696b774c695a716455714d64355a4b5147596151543463584d65566a660a0a39424143443543303a2078707562364565667243724d416475684e776e734862336441733844595a53773466363357795236446145427955486a777650446468637a6a31354679424247347462454a74663476524b5476316e67355350506e57763150766531663135454a66694259356f59444e36564c45430a0a",
},
}
for _, test := range tests {
enc, err := hex.DecodeString(test.enc)
if err != nil {
t.Fatal(err)
}
got, err := Parse("bytes", enc)
if err != nil {
t.Fatal(err)
}
gotHex := hex.EncodeToString(got.([]byte))
if gotHex != test.want {
t.Errorf("%s\ndecoded to:\n%s\nwanted\n%s", test.enc, gotHex, test.want)
}
}
}
func TestHDKey(t *testing.T) {
tests := []struct {
k KeyDescriptor
want string
}{
{
KeyDescriptor{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0xdd4fadee,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 48, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart + 2},
KeyData: []byte{0x2, 0x21, 0x96, 0xad, 0xc2, 0x5f, 0xde, 0x16, 0x9f, 0xe9, 0x2e, 0x70, 0x76, 0x90, 0x59, 0x10, 0x22, 0x75, 0xd2, 0xb4, 0xc, 0xc9, 0x87, 0x76, 0xea, 0xab, 0x92, 0xb8, 0x2a, 0x86, 0x13, 0x5e, 0x92},
ChainCode: []byte{0x43, 0x8e, 0xff, 0x7b, 0x3b, 0x36, 0xb6, 0xd1, 0x1a, 0x60, 0xa2, 0x2c, 0xcb, 0x93, 0x6, 0xee, 0xa3, 0x5, 0xb0, 0x43, 0x9f, 0x1e, 0xa0, 0x9d, 0x59, 0x28, 0x1, 0x5d, 0xe3, 0x73, 0x81, 0x16},
ParentFingerprint: 0x22969377,
},
"a4035821022196adc25fde169fe92e70769059102275d2b40cc98776eaab92b82a86135e92045820438eff7b3b36b6d11a60a22ccb9306eea305b0439f1ea09d5928015de373811606d90130a201881830f500f500f502f5021add4fadee081a22969377",
},
{
KeyDescriptor{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0xbd16bee5,
DerivationPath: Path{0},
Children: []Derivation{
{Index: 0},
{Index: 0},
{Type: WildcardDerivation},
},
KeyData: []byte{0x2, 0xfc, 0x9e, 0x5a, 0xf0, 0xac, 0x8d, 0x9b, 0x3c, 0xec, 0xfe, 0x2a, 0x88, 0x8e, 0x21, 0x17, 0xba, 0x3d, 0x8, 0x9d, 0x85, 0x85, 0x88, 0x6c, 0x9c, 0x82, 0x6b, 0x6b, 0x22, 0xa9, 0x8d, 0x12, 0xea},
ChainCode: []byte{0xf0, 0x90, 0x9a, 0xff, 0xaa, 0x7e, 0xe7, 0xab, 0xe5, 0xdd, 0x4e, 0x10, 0x5, 0x98, 0xd4, 0xdc, 0x53, 0xcd, 0x70, 0x9d, 0x5a, 0x5c, 0x2c, 0xac, 0x40, 0xe7, 0x41, 0x2f, 0x23, 0x2f, 0x7c, 0x9c},
ParentFingerprint: 0x00000000,
},
"a403582102fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea045820f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c06d90130a2018200f4021abd16bee507d90130a1018600f400f480f4",
},
}
for _, test := range tests {
got := test.k.Encode()
gotHex := hex.EncodeToString(got)
if gotHex != test.want {
t.Errorf("key:\n%+v\nencoded to:%s\nwanted: %s\n", test.k, gotHex, test.want)
}
parsed, err := Parse("crypto-hdkey", got)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(parsed, test.k) {
t.Errorf("key:\n%+v\nroundtripped to\n%+v\n", test.k, parsed)
}
}
}
func TestCryptoAccount(t *testing.T) {
tests := []struct {
d OutputDescriptor
enc string
}{
{
OutputDescriptor{
Script: P2WPKH, Threshold: 1, Keys: []KeyDescriptor{
{
Network: &chaincfg.MainNetParams,
MasterFingerprint: 0x4bbaa801,
DerivationPath: Path{hdkeychain.HardenedKeyStart + 84, hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart},
KeyData: []uint8{0x2, 0xa1, 0xe9, 0xcd, 0x9e, 0xfc, 0x5, 0x1f, 0x3e, 0x3, 0x74, 0xbf, 0x21, 0x39, 0x90, 0xd2, 0x3b, 0xf3, 0xd7, 0x7f, 0xdd, 0xf1, 0x72, 0xbc, 0xc6, 0x23, 0x43, 0xc4, 0xd7, 0x82, 0xe7, 0x80, 0xec},
ChainCode: []uint8{0x3f, 0xac, 0x4d, 0x0, 0x92, 0x28, 0x2, 0xa9, 0xf2, 0xbd, 0x52, 0xc, 0xc4, 0x51, 0x22, 0x30, 0xcf, 0x29, 0xb, 0x4a, 0x5d, 0x29, 0x7e, 0x5d, 0x3a, 0x69, 0xb9, 0x9f, 0x6, 0x57, 0x7f, 0x66},
ParentFingerprint: 0x43ecdeeb,
},
},
},
"a2011a4bbaa8010281d90194d9012fa403582102a1e9cd9efc051f3e0374bf213990d23bf3d77fddf172bcc62343c4d782e780ec0458203fac4d00922802a9f2bd520cc4512230cf290b4a5d297e5d3a69b99f06577f6606d90130a301861854f500f500f5021a4bbaa8010303081a43ecdeeb",
},
}
for _, test := range tests {
enc, err := hex.DecodeString(test.enc)
if err != nil {
t.Fatal(err)
}
parsed, err := Parse("crypto-account", enc)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(parsed, test.d) {
t.Errorf("crypto-account:\n%s\ndecoded to\n%+v\nexpected\n%+v", test.enc, parsed, test.d)
}
}
}
func TestIncompleteCryptoAccount(t *testing.T) {
const enc = "a2011a4bbaa8010281d90191d9012fa4035821024eaf73e5f71a386667c34795a955316fdcd0cf8e2bb99defa6a3619cdc6c29140458208f34522cb231cd7e957a8e881798748b90b6dce5488d4140ee14c1ea40abf0de06d90130a301881830f500f500f502f5021a4bbaa8010304081a51cb0a5b"
bytes, err := hex.DecodeString(enc)
if err != nil {
t.Fatal(err)
}
if _, err := Parse("crypto-account", bytes); err == nil {
t.Fatalf("invalid crypto-account %s parsed succesfully", enc)
}
}

51
bc/xoshiro256/xoshiro.go Normal file
View File

@ -0,0 +1,51 @@
// Package xoshiro256 implements the xoshiro256** pseudo-random
// number generator. The implementation is based on the public domain
// [C implementation].
//
// [C implementation]: https://xoshiro.di.unimi.it/xoshiro256starstar.c
package xoshiro256
import (
"encoding/binary"
"math"
)
type Source struct {
state [4]uint64
}
func (s *Source) Seed(seed [32]byte) {
s.state[0] = binary.BigEndian.Uint64(seed[0:8])
s.state[1] = binary.BigEndian.Uint64(seed[8:16])
s.state[2] = binary.BigEndian.Uint64(seed[16:24])
s.state[3] = binary.BigEndian.Uint64(seed[24:32])
}
func (s *Source) Uint64() uint64 {
result := rotl(s.state[1]*5, 7) * 9
t := s.state[1] << 17
s.state[2] ^= s.state[0]
s.state[3] ^= s.state[1]
s.state[1] ^= s.state[2]
s.state[0] ^= s.state[3]
s.state[2] ^= t
s.state[3] = rotl(s.state[3], 45)
return result
}
func (s *Source) Intn(n int) int {
return int(s.Float64() * float64(n))
}
func (s *Source) Float64() float64 {
return float64(s.Uint64()) / (float64(math.MaxUint64) + 1)
}
func rotl(x uint64, k int) uint64 {
return (x << k) | (x >> (64 - k))
}

View File

@ -0,0 +1,42 @@
package xoshiro256
import (
"bytes"
"encoding/hex"
"testing"
)
func TestGenerator(t *testing.T) {
tests := []struct {
seed string
want string
}{
{
"ea858afbf837aae714617e89a36524aced28f7de921f7798e72810fd8839a462",
"2a51550852544c494658024a28304d36580705582519520d453b1e270b5213632d571e0f2016592c5c4d1d4e045c2c445c45012a593225543f222003113e28625259182b55270f03631d142a1b0a554232234546464a1e0d48360b0546375b340a2b2b34",
},
{
"530c1f0542883298051e4efa4adbf209c7f9d8e794fb62fd3fd4b48739694080",
"582c5e4a0063074d44232f4e1315320f2a245b0b55274016390b190c015b114b1d2f580b443a1b4115362f364953173a4b1b1a0f3c241e1537394d4c4b2f354c095b0e45035f0b491463443d0362246238410e504a393f4433381827355039335103011e",
},
}
for _, test := range tests {
seed, err := hex.DecodeString(test.seed)
if err != nil {
t.Fatal(err)
}
want, err := hex.DecodeString(test.want)
if err != nil {
t.Fatal(err)
}
var s Source
s.Seed(([32]byte)(seed))
got := make([]byte, len(want))
for i := 0; i < len(want); i++ {
got[i] = byte(s.Uint64() % 100)
}
if !bytes.Equal(got, want) {
t.Errorf("unexpected random number sequence for seed %x", seed)
}
}
}

23
bip32/bip32.go Normal file
View File

@ -0,0 +1,23 @@
// package bip32 contains helper functions for operating on bitcoin bip32
// extended keys.
package bip32
import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/mineracks/seedhammer-v1-companion/bc/urtypes"
)
func Derive(mk *hdkeychain.ExtendedKey, path urtypes.Path) (mfp uint32, xpub *hdkeychain.ExtendedKey, err error) {
key := mk
for i, p := range path {
key, err = key.Derive(p)
if err != nil {
return
}
if i == 0 {
mfp = key.ParentFingerprint()
}
}
xpub, err = key.Neuter()
return
}

220
bip39/bip39.go Normal file
View File

@ -0,0 +1,220 @@
// package bip39 represents and converts bitcoin bip39 mnemonic phrases.
package bip39
//go:generate go run gen.go
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"
"sort"
"strings"
"golang.org/x/crypto/pbkdf2"
)
type Word int
type Mnemonic []Word
type Roll [5]int
const NumWords = Word(len(index))
var ErrInvalidChecksum = errors.New("bip39: invalid checksum")
// DiceToWord converts a dice roll to its bip39 word index. It returns
// false if the roll doesn't have a word defined.
func DiceToWord(roll Roll) (Word, bool) {
// Map 1-6 to 0-5; fail if out of bounds.
for i, digit := range roll {
digit--
if digit < 0 || 5 < digit {
return -1, false
}
// Last digit is a coin toss.
if i == len(roll)-1 {
if digit < 3 {
digit = 0
} else {
digit = 1
}
}
roll[i] = digit
}
const rowsPerSubcolumn = 5 * 16
const rowsPerPage = 13 * 16
const wordsPerPage = 2 * rowsPerPage
page := roll[0]
subcol := roll[len(roll)-1]
row := 0
exp := 1
// Compute row from middle dice.
for i := len(roll) - 1 - 1; i >= 1; i-- {
row += roll[i] * exp
exp *= 6
}
if row >= rowsPerPage {
return -1, false
}
column := row / rowsPerSubcolumn
word := column*2*rowsPerSubcolumn + row%rowsPerSubcolumn
subrows := (rowsPerPage - column*rowsPerSubcolumn)
if subrows > rowsPerSubcolumn {
subrows = rowsPerSubcolumn
}
word += subrows * subcol
word += page * 2 * rowsPerPage
w := Word(word)
if !w.valid() {
return -1, false
}
return w, true
}
func LabelFor(w Word) string {
if !w.valid() {
return ""
}
start := index[w]
end := uint16(len(words))
if int(w+1) < len(index) {
end = index[w+1]
}
return words[start:end]
}
func (w Word) valid() bool {
return w >= 0 && int(w) < len(index)
}
func ClosestWord(word string) (Word, bool) {
i := sort.Search(len(index), func(i int) bool {
return LabelFor(Word(i)) >= word
})
if i == len(index) {
return -1, false
}
match := LabelFor(Word(i))
return Word(i), strings.HasPrefix(match, word)
}
// Valid reports whether the mnemonic checksum is correct.
func (m Mnemonic) Valid() bool {
// Panics in splitMnemonic.
if len(m)%3 != 0 {
return false
}
ent, _ := splitMnemonic(m)
last := m[len(m)-1]
return ChecksumWord(ent) == last
}
// FixChecksum returns a copy of the mnemonic with a correct checksum.
// This method defeats the purpose of the bip39 checksum, so it should
// only be used for generating new mnemonics.
func (m Mnemonic) FixChecksum() Mnemonic {
m2 := make(Mnemonic, len(m))
copy(m2, m)
ent, _ := splitMnemonic(m2)
m2[len(m2)-1] = ChecksumWord(ent)
return m2
}
// Entropy returns the entropy represented by the mnemonic. It
// panics if the mnemonic is invalid.
func (m Mnemonic) Entropy() []byte {
if !m.Valid() {
panic("invalid mnemonic")
}
ent, _ := splitMnemonic(m)
return ent
}
func splitMnemonic(m Mnemonic) (entropy []byte, checksum byte) {
ent := big.NewInt(0)
const wordBits = 11
shift11 := big.NewInt(1 << wordBits)
for _, w := range m {
ent.Mul(ent, shift11)
ent.Or(ent, big.NewInt(int64(w)))
}
if len(m)%3 != 0 {
panic("mnemonic length not divisible with 3")
}
checkBits := len(m) / 3
check := big.NewInt(0).And(ent, big.NewInt(1<<checkBits-1)).Int64()
ent.Div(ent, big.NewInt(1<<checkBits))
// Pad entropy bytes because BIP39 checksum is sensitive to
// leading zeros.
entBits := len(m)*wordBits - checkBits
entBytes := ent.Bytes()
padding := bytes.Repeat([]byte{0}, entBits/8-len(entBytes))
entBytes = append(padding, entBytes...)
return entBytes, byte(check)
}
func Checksum(entropy []byte) byte {
h := sha256.New()
h.Write(entropy)
check := h.Sum(nil)[0]
checkBits := len(entropy) / 4
if checkBits > 8 {
panic("entropy too long")
}
return check >> (8 - checkBits)
}
func ChecksumWord(entropy []byte) Word {
checkBits := len(entropy) / 4
last := entropy[len(entropy)-1]
w := Word(last)<<checkBits | Word(Checksum(entropy))
return w % Word(len(index))
}
func MnemonicSeed(m Mnemonic, password string) []byte {
var sentence strings.Builder
for i, w := range m {
sentence.WriteString(LabelFor(w))
if i < len(m)-1 {
sentence.WriteByte(' ')
}
}
return pbkdf2.Key([]byte(sentence.String()), []byte("mnemonic"+password), 2048, 64, sha512.New)
}
func ParseMnemonic(mnemonic string) (Mnemonic, error) {
words := strings.Split(mnemonic, " ")
bip39s := make(Mnemonic, len(words))
for i, w := range words {
closest, valid := ClosestWord(w)
if !valid || LabelFor(closest) != w {
return nil, fmt.Errorf("bip39: unknown word: %q", w)
}
bip39s[i] = closest
}
if !bip39s.Valid() {
return nil, ErrInvalidChecksum
}
return bip39s, nil
}
func RandomWord() Word {
var u16 [2]byte
if _, err := rand.Read(u16[:]); err != nil {
panic(err)
}
// Modulo reduction of a random number ok because the reduced
// range (2^11) divides the full range (2^16). But be paranoid.
const n = len(index)
if math.MaxUint16%n != n-1 {
panic("biased random distribution")
}
return Word(binary.BigEndian.Uint16(u16[:])) % Word(n)
}

187
bip39/bip39_test.go Normal file
View File

@ -0,0 +1,187 @@
package bip39
import (
"bytes"
"encoding/hex"
"testing"
)
func TestVectors(t *testing.T) {
for _, v := range testVectors {
m, err := ParseMnemonic(v.mnemonic)
if err != nil {
t.Fatalf("mnemonic %q failed to parse: %v", v.mnemonic, err)
}
e, err := hex.DecodeString(v.entropy)
if err != nil {
t.Error(err)
}
ent, check := splitMnemonic(m)
if !bytes.Equal(e, ent) {
t.Errorf("entropy mismatch")
}
if want := Checksum(ent); want != check {
t.Errorf("checksum mismatch, got %d, want %d", check, want)
}
checkWord := m[len(m)-1]
if want := ChecksumWord(ent); want != checkWord {
t.Errorf("checksum word mismatch, got %d, want %d", checkWord, want)
}
}
}
func TestInvalidSeeds(t *testing.T) {
tests := []string{
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
}
for _, test := range tests {
_, err := ParseMnemonic(test)
if err == nil {
t.Errorf("successfully parsed invalid seed %q", test)
}
}
}
func TestChecksumWord(t *testing.T) {
mnemonic := make(Mnemonic, 12)
for i := 0; i < 1e4; i++ {
for j := range mnemonic {
mnemonic[j] = RandomWord()
}
want, _ := splitMnemonic(mnemonic)
got := mnemonic.FixChecksum().Entropy()
if !bytes.Equal(want, got) {
t.Errorf("checksum word changed the entropy")
}
}
}
var testVectors = []struct {
entropy string
mnemonic string
}{
{
entropy: "00000000000000000000000000000000",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
},
{
entropy: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
mnemonic: "legal winner thank year wave sausage worth useful legal winner thank yellow",
},
{
entropy: "80808080808080808080808080808080",
mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
},
{
entropy: "ffffffffffffffffffffffffffffffff",
mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
},
{
entropy: "000000000000000000000000000000000000000000000000",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
},
{
entropy: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
mnemonic: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
},
{
entropy: "808080808080808080808080808080808080808080808080",
mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
},
{
entropy: "ffffffffffffffffffffffffffffffffffffffffffffffff",
mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
},
{
entropy: "0000000000000000000000000000000000000000000000000000000000000000",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
},
{
entropy: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
mnemonic: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
},
{
entropy: "8080808080808080808080808080808080808080808080808080808080808080",
mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
},
{
entropy: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
},
{
entropy: "9e885d952ad362caeb4efe34a8e91bd2",
mnemonic: "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
},
{
entropy: "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
mnemonic: "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
},
{
entropy: "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
mnemonic: "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
},
{
entropy: "c0ba5a8e914111210f2bd131f3d5e08d",
mnemonic: "scheme spot photo card baby mountain device kick cradle pact join borrow",
},
{
entropy: "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
mnemonic: "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
},
{
entropy: "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
mnemonic: "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
},
{
entropy: "23db8160a31d3e0dca3688ed941adbf3",
mnemonic: "cat swing flag economy stadium alone churn speed unique patch report train",
},
{
entropy: "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
mnemonic: "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
},
{
entropy: "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
mnemonic: "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
},
{
entropy: "f30f8c1da665478f49b001d94c5fc452",
mnemonic: "vessel ladder alter error federal sibling chat ability sun glass valve picture",
},
{
entropy: "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
mnemonic: "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
},
{
entropy: "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
mnemonic: "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
},
}
func TestDiceToWord(t *testing.T) {
counts := make([]int, len(index))
dice := Roll{1, 1, 1, 1, 1}
loop:
for {
word, valid := DiceToWord(dice)
// Increment roll.
for i := len(dice) - 1; ; i-- {
if i < 0 {
break loop
}
dice[i]++
if dice[i] <= 6 {
break
}
dice[i] = 1
}
if valid {
counts[word]++
}
}
for word, count := range counts {
if count != 3 {
t.Errorf("word %v chosen %d times, expected 3", word, count)
}
}
}

16
bip39/doc.go Normal file
View File

@ -0,0 +1,16 @@
// Package bip39 is the BIP39 mnemonic seed-phrase encoder/decoder + the
// 2048-word English wordlist baked in as data.
//
// Used by:
// - The composer to take a user-typed/scanned seed and validate it
// before designing a plate
// - The Pi controller during the actual engrave flow
// - The SeedSigner sim bridge for the QR-handoff bus
//
// LIFTED from upstream seedhammer/seedhammer at v1.3.0
// (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e). Hardware-agnostic;
// the v1.3.0 baseline matches what backup/ was tested against.
//
// gen.go has //go:build ignore — it's the wordlist generator, run via
// `go generate`, not part of the build.
package bip39

2102
bip39/gen.go Normal file

File diff suppressed because it is too large Load Diff

10
bip39/wordlist.go Normal file

File diff suppressed because one or more lines are too long

12
driver/mjolnir/doc.go Normal file
View File

@ -0,0 +1,12 @@
// Package mjolnir drives the MarkingWay engraving machine over USB serial.
//
// "Mjolnir" because it talks to the hammer. The wire protocol is documented
// in docs/architecture/v1-engrave-spec.md.
//
// LIFTED from upstream seedhammer/seedhammer at v1.3.0
// (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e) path driver/mjolnir/.
// On a real device this opens /dev/ttyUSB0 via github.com/tarm/serial; in
// the browser emulator the open call is intercepted by platform/v1's
// browser adapter and routed to a null sink (preview) or animation harness
// (visual playback).
package mjolnir

388
driver/mjolnir/driver.go Executable file
View File

@ -0,0 +1,388 @@
// package mjolnir implements a driver for the MarkingWay engraving
// machine.
package mjolnir
import (
"bufio"
"bytes"
"errors"
"fmt"
"image"
"io"
"runtime"
"github.com/tarm/serial"
"github.com/mineracks/seedhammer-v1-companion/engrave"
)
type program struct {
cmds chan [cmdSize]byte
count int
sent int
}
var Params = engrave.Params{
StrokeWidth: 38,
Millimeter: 126,
}
type Options struct {
MoveSpeed float32
PrintSpeed float32
End image.Point
}
var safePoint = image.Pt(119, 43)
const (
cmdSize = 10
defaultMoveSpeed = .5
defaultPrintSpeed = .1
)
func Open(dev string) (io.ReadWriteCloser, error) {
// Hardware parameters.
const (
baudRate = 115200
stopBits = 1
parity = false
wordLen = 8
controlHandshake = 0
flowReplace = 0
xonLimit = 2048
xoffLimit = 512
)
var devices []string
if dev != "" {
devices = append(devices, dev)
} else {
switch runtime.GOOS {
case "windows":
devices = append(devices, "COM3")
case "linux":
devices = append(devices, "/dev/ttyUSB0", "/dev/ttyUSB1")
}
}
if len(devices) == 0 {
return nil, errors.New("no device specified")
}
var firstErr error
for _, dev := range devices {
c := &serial.Config{Name: dev, Baud: baudRate}
s, err := serial.OpenPort(c)
if err == nil {
return s, nil
}
if firstErr == nil {
firstErr = err
}
}
return nil, firstErr
}
const (
initCmd = 0x00
cancelCmd = 0xaf
setSpeedCmd = 0x30
setDelaysCmd = 0x31
moveToOriginCmd = 0x21
moveToOriginCmdExtra = 0x50
moveToOriginCmdResponse = 0x00
initProgramCmd = 0x60
moveCmd = 0x80
lineCmd = 0x00
nopCmd = 0xff
)
const (
initializedStatus = 0x00
cancellingStatus = 0x62
cancelledStatus = 0x65
bufferProgramStatus = 0x60
programStepStatus = 0x6f
programCompleteStatus = 0x6a
)
// The engraver expects program commands in batches.
const progBatchSize = 80
func Engrave(dev io.ReadWriter, opts Options, plan engrave.Plan, quit <-chan struct{}) (eerr error) {
bufw := bufio.NewWriterSize(dev, progBatchSize*cmdSize)
writeMut := make(chan struct{}, 1)
writeMut <- struct{}{}
flush := func() {
<-writeMut
defer func() { writeMut <- struct{}{} }()
if eerr != nil {
return
}
eerr = bufw.Flush()
}
defer flush()
wr := func(data ...byte) {
<-writeMut
defer func() { writeMut <- struct{}{} }()
if eerr != nil {
return
}
_, eerr = bufw.Write(data)
}
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-quit:
select {
case <-writeMut:
case <-done:
return
}
dev.Write([]byte{cancelCmd})
writeMut <- struct{}{}
<-done
case <-done:
}
}()
bufr := bufio.NewReaderSize(dev, 100)
r := func(c int) []byte {
flush()
if eerr != nil {
return nil
}
data := make([]byte, c)
n, err := bufr.Read(data)
eerr = err
data = data[:n]
return data
}
expect := func(exp ...byte) {
for len(exp) > 0 && eerr == nil {
got := r(len(exp))
n := len(got)
if !bytes.Equal(exp[:n], got) {
eerr = fmt.Errorf("unexpected reply\nexp: %#x\ngot: %#x", exp, got)
return
}
exp = exp[n:]
}
}
atleast := func(n int) []byte {
var res []byte
for n > 0 {
data := r(n)
res = append(res, data...)
n -= len(data)
}
return res
}
origin := func() {
wr(moveToOriginCmd, moveToOriginCmdExtra)
expect(moveToOriginCmd, moveToOriginCmdResponse)
}
cancel := func() {
wr(cancelCmd)
}
initialize := func() {
cancel()
wr(initCmd)
for {
status := r(1)
if eerr != nil {
break
}
switch status[0] {
case initializedStatus:
return
case cancelledStatus:
// Re-initialize.
wr(initCmd)
}
}
}
parseCoords := func(coords []byte) (x int, y int, z int) {
x = int(coords[0]) | int(coords[1])<<8 | int(coords[2])<<16
y = int(coords[3]) | int(coords[4])<<8 | int(coords[5])<<16
z = int(coords[6]) | int(coords[7])<<8 | int(coords[8])<<16
return
}
queryPos := func() (x int, y int, z int) {
wr(0x16)
expect(0x16)
x, y, z = parseCoords(atleast(9))
return
}
_, _ = atleast, queryPos
initialize()
// Speed range: [1000,30].
setSpeeds := func(print, move, xxx int) {
wr(setSpeedCmd, byte(print), byte(print>>8), byte(move), byte(move>>8), byte(xxx), byte(xxx>>8))
expect(setSpeedCmd)
}
// Delay range: 0-255.
setDelays := func(penDown, penUp int) {
wr(setDelaysCmd, byte(penDown), byte(penUp))
expect(setDelaysCmd)
}
setDelays(0x14, 0x14)
// Init done.
runProgram := func(plan engrave.Plan) {
p := &program{}
plan(p.Command)
p.Prepare()
defer func() {
for i := p.sent; i < p.count; i++ {
<-p.cmds
}
}()
go plan(p.Command)
p.sent = 0
// Round up to nearest batch size. Note that the rounding
// adds another, empty, batch in case we fill up the last one.
// Otherwise, the engraver won't send a completed status.
nbatches := (p.count + progBatchSize) / progBatchSize
if nbatches > 0xffff {
eerr = errors.New("engrave: program too large")
return
}
wr(initProgramCmd, byte(nbatches), byte(nbatches>>8))
done:
for {
status := r(1)
if eerr != nil {
return
}
paddedCount := nbatches * progBatchSize
switch status[0] {
case bufferProgramStatus:
if p.sent == paddedCount {
break
}
ncmd := progBatchSize
if rem := p.count - p.sent; ncmd > rem {
ncmd = rem
}
for i := 0; i < ncmd; i++ {
cmd := <-p.cmds
p.sent++
wr(cmd[:]...)
}
// Pad with 0xff.
pad := [cmdSize]byte{}
for i := range pad {
pad[i] = nopCmd
}
for i := ncmd; i < progBatchSize; i++ {
p.sent++
wr(pad[:]...)
}
case programStepStatus:
case programCompleteStatus:
break done
case cancellingStatus:
case cancelledStatus:
if eerr == nil {
eerr = ErrCancelled
}
}
}
}
moveTo := func(p image.Point) {
runProgram(func(yield func(engrave.Command)) {
yield(engrave.Move(p))
})
}
setSpeeds(300, 300, 0xe6)
// Prepare the machine: (1) reset the origin and
// (2) move to safe point. The first is necessary because
// the absolute position of the needle is not known at startup.
// The second is to avoid needle collision with the tightening
// nuts.
origin()
// Avoid a false home by moving out and re-homing.
falseHome := 5 * Params.Millimeter
moveTo(image.Pt(falseHome, falseHome))
origin()
sp := image.Point{
X: safePoint.X * Params.Millimeter,
Y: safePoint.Y * Params.Millimeter,
}
moveTo(sp)
// 0 lowest, 1 highest.
moveSpeed := opts.MoveSpeed
printSpeed := opts.PrintSpeed
if moveSpeed == 0 {
moveSpeed = defaultMoveSpeed
}
if printSpeed == 0 {
printSpeed = defaultPrintSpeed
}
mms := int(moveSpeed*float32(30) + (1.-moveSpeed)*float32(1000))
mps := int(printSpeed*float32(30) + (1.-printSpeed)*float32(1000))
setSpeeds(mps, mms, 0xe6)
runProgram(plan)
if eerr == nil || eerr == ErrCancelled {
setSpeeds(300, 300, 0xe6)
if opts.End != (image.Point{}) {
moveTo(opts.End)
} else {
moveTo(sp)
origin()
}
}
return eerr
}
var ErrCancelled = errors.New("cancelled")
func mkcoords(p image.Point) [9]byte {
x, y := p.X, p.Y
if x < 0 || x > 0xffffff || y < 0 || y > 0xffffff {
panic(fmt.Errorf("(%d,%d) out of range", x, y))
}
return [...]byte{
byte(x), byte(x >> 8), byte(x >> 16),
byte(y), byte(y >> 8), byte(y >> 16),
0x00, 0x00, 0x00, // Z = 0.
}
}
func (p *program) cmd(c [cmdSize]byte) {
if p.cmds != nil {
p.cmds <- c
} else {
p.count++
}
}
func (p *program) Prepare() {
p.cmds = make(chan [cmdSize]byte)
}
func (p *program) Command(c engrave.Command) {
var cmd [cmdSize]byte
coords := mkcoords(c.Coord)
copy(cmd[1:], coords[:])
if c.Line {
cmd[0] = lineCmd
} else {
cmd[0] = moveCmd
}
p.cmd(cmd)
p.pause()
}
func (p *program) pause() {
// p.cmd([...]byte{0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
}

24
driver/mjolnir/driver_test.go Executable file
View File

@ -0,0 +1,24 @@
package mjolnir
import (
"image"
"testing"
"github.com/mineracks/seedhammer-v1-companion/engrave"
)
func TestEndToEnd(t *testing.T) {
s := NewSimulator()
defer s.Close()
design := func(yield func(engrave.Command)) {
for i := 0; i < 2000; i++ {
yield(engrave.Line(image.Pt(i, i*2)))
yield(engrave.Line(image.Pt(i*4, i*3)))
yield(engrave.Move(image.Pt(i, i)))
}
}
if err := Engrave(s, Options{}, design, nil); err != nil {
t.Error(err)
}
}

204
driver/mjolnir/sim.go Normal file
View File

@ -0,0 +1,204 @@
package mjolnir
import (
"errors"
)
type Simulator struct {
state deviceState
ncmds int
nbuffered int
Cmds []Cmd
close chan struct{}
in chan ioRequest
out chan ioResult
}
type Cmd struct {
Type CmdType
X, Y uint32
}
type CmdType int
const (
MoveTo CmdType = iota
LineTo
)
func NewSimulator() *Simulator {
sim := &Simulator{
close: make(chan struct{}),
in: make(chan ioRequest),
out: make(chan ioResult),
}
go sim.run()
return sim
}
type deviceState int
const (
stateReady deviceState = iota
stateInitializing
stateSetSpeed
stateSetDelays
stateMoveToOrigin
stateExecuting
)
type ioRequest struct {
write bool
data []byte
}
type ioResult struct {
bytes int
err error
}
func (s *Simulator) run() {
for {
select {
case <-s.close:
s.close <- struct{}{}
return
case r := <-s.in:
var n int
var err error
if r.write {
n, err = s.doWrite(r.data)
} else {
n, err = s.doRead(r.data)
}
s.out <- ioResult{n, err}
}
}
}
func coordsFromCmd(cmd []byte) (uint32, uint32) {
x := uint32(cmd[0]) | uint32(cmd[1])<<8 | uint32(cmd[2])<<16
y := uint32(cmd[3]) | uint32(cmd[4])<<8 | uint32(cmd[5])<<16
return x, y
}
func (s *Simulator) doRead(data []byte) (int, error) {
read := func(resp []byte) (int, error) {
if len(resp) > len(data) {
return 0, errors.New("read overflow")
}
copy(data, resp)
return len(resp), nil
}
switch s.state {
case stateInitializing:
s.state = stateReady
return read([]byte{initializedStatus})
case stateSetSpeed:
s.state = stateReady
return read([]byte{setSpeedCmd})
case stateSetDelays:
s.state = stateReady
return read([]byte{setDelaysCmd})
case stateMoveToOrigin:
s.state = stateReady
return read([]byte{moveToOriginCmd, moveToOriginCmdResponse})
case stateExecuting:
switch {
case s.nbuffered == 0 && s.ncmds > 0:
return read([]byte{bufferProgramStatus})
case s.nbuffered == 0 && s.ncmds == 0:
return read([]byte{programCompleteStatus})
default:
s.nbuffered--
return read([]byte{programStepStatus})
}
default:
return 0, errors.New("invalid device state")
}
}
func (s *Simulator) doWrite(data []byte) (n int, err error) {
skip := func(bytes int) {
if len(data) < bytes {
err = errors.New("buffer underflow")
return
}
n += bytes
data = data[bytes:]
}
read := func(bytes int) []byte {
res := make([]byte, bytes)
copy(res, data)
skip(bytes)
return res
}
batchCmd := func() {
s.nbuffered++
s.ncmds--
skip(9)
}
for len(data) > 0 {
n += 1
cmd := data[0]
data = data[1:]
switch cmd {
case cancelCmd:
s.state = stateReady
case initCmd:
if s.state == stateExecuting {
// 0x00 is line to in programming mode.
x, y := coordsFromCmd(data)
s.Cmds = append(s.Cmds, Cmd{LineTo, x, y})
batchCmd()
} else {
s.state = stateInitializing
}
case setSpeedCmd:
s.state = stateSetSpeed
skip(6)
case setDelaysCmd:
s.state = stateSetDelays
skip(2)
case moveToOriginCmd:
s.state = stateMoveToOrigin
subCmd := read(1)
if err == nil && subCmd[0] != moveToOriginCmdExtra {
err = errors.New("invalid origin command")
}
s.Cmds = append(s.Cmds, Cmd{MoveTo, 0, 0})
case initProgramCmd:
s.state = stateExecuting
ncmds := read(2)
s.ncmds = (int(ncmds[0]) | int(ncmds[1])<<8) * progBatchSize
case moveCmd:
x, y := coordsFromCmd(data)
s.Cmds = append(s.Cmds, Cmd{MoveTo, x, y})
batchCmd()
case nopCmd:
batchCmd()
default:
return n, errors.New("invalid command")
}
}
return
}
func (s *Simulator) Read(data []byte) (int, error) {
s.in <- ioRequest{false, data}
r := <-s.out
return r.bytes, r.err
}
func (s *Simulator) Write(data []byte) (int, error) {
s.in <- ioRequest{true, data}
r := <-s.out
return r.bytes, r.err
}
func (s *Simulator) Close() error {
s.close <- struct{}{}
<-s.close
return nil
}

View File

@ -1,20 +1,13 @@
// Package engrave converts plate designs into MarkingWay engraver commands. // Package engrave transforms shapes such as text and QR codes into line
// and move commands for the engraver.
// //
// Wraps three concerns: // LIFTED from upstream seedhammer/seedhammer at v1.3.0
// (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e). Hardware-agnostic in
// the sense that it produces command streams that any compatible engraver
// can consume; the actual byte-level wire encoding lives in
// engrave/wire/ + driver/mjolnir/.
// //
// - Geometry: take a layout (text + SVG paths + plate type) and produce // Subpackages:
// a stream of MoveTo/LineTo commands in the engraver's coordinate // - engrave/wire/sh1e/ — the SH1E plate-design envelope (new code, ours)
// system (machine steps, 1 step ≈ 0.00796 mm). // - engrave/wire/ — the live MarkingWay USB-serial encoder (future)
//
// - Tessellation: convert higher-order curves (Quad/Cube/Spline) into
// line segments at the engraver's resolution. Uses bezier/ + bspline/.
//
// - Wire: serialise the command stream into the 10-byte binary frames
// the MarkingWay protocol expects. See engrave/wire/ for the on-the-wire
// formats (engraver USB-serial as well as SH1E for QR transport).
//
// Status: STUB — to be lifted from upstream seedhammer/seedhammer at v1.3.0
// (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e), specifically the
// engrave package. See docs/architecture/v1-engrave-spec.md for the
// audit of the v1 wire protocol that this package targets.
package engrave package engrave

1110
engrave/engrave.go Executable file

File diff suppressed because it is too large Load Diff

99
engrave/engrave_test.go Normal file
View File

@ -0,0 +1,99 @@
package engrave
import (
"image"
"io"
"math/rand"
"reflect"
"strings"
"testing"
"github.com/kortschak/qr"
"github.com/mineracks/seedhammer-v1-companion/bip39"
"github.com/mineracks/seedhammer-v1-companion/font/constant"
)
func TestConstantQR(t *testing.T) {
rng := rand.New(rand.NewSource(44))
for i := 0; i < 100; i++ {
for n := 16; n <= 32; n++ {
entropy := make([]byte, n)
if _, err := io.ReadFull(rng, entropy); err != nil {
t.Fatal(err)
}
lvl := qr.Q
cmd, err := constantQR(7, 4, lvl, entropy)
if err != nil {
t.Fatalf("entropy: %x: %v", entropy, err)
}
qrc, err := qr.Encode(string(entropy), lvl)
if err != nil {
t.Fatal(err)
}
dim := qrc.Size
want := bitmapForQR(qrc)
_, _, got := bitmapForQRStatic(dim)
for _, p := range cmd.plan {
got.Set(p)
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("entropy: %x: engraving plan doesn't match QR code", entropy)
}
}
}
}
func TestConstantString(t *testing.T) {
s := NewConstantStringer(constant.Font, 1000, bip39.ShortestWord, bip39.LongestWord)
for i := bip39.Word(0); i < bip39.NumWords; i++ {
w := strings.ToUpper(bip39.LabelFor(i))
cmd := s.String(w)
bounds := image.Rect(0, 0, s.longest*s.dims.X, s.dims.Y)
moves := measureMoves(cmd)
if !moves.In(bounds) {
t.Errorf("%s movement bounds %v are not inside bounds %v", w, moves, bounds)
}
}
}
func FuzzConstantQR(f *testing.F) {
f.Fuzz(func(t *testing.T, entropy []byte) {
if len(entropy) < 16 {
return
}
if len(entropy) > 32 {
entropy = entropy[:32]
}
if _, err := ConstantQR(1, 3, qr.Q, entropy); err != nil {
t.Fatalf("entropy: %x: %v", entropy, err)
}
if _, err := ConstantQR(1, 3, qr.L, entropy); err != nil {
t.Fatalf("entropy: %x: %v", entropy, err)
}
})
}
func measureMoves(p Plan) image.Rectangle {
inf := image.Rectangle{Min: image.Pt(1e6, 1e6), Max: image.Pt(-1e6, -1e6)}
bounds := inf
p(func(cmd Command) {
if cmd.Line {
return
}
p := cmd.Coord
if p.X < bounds.Min.X {
bounds.Min.X = p.X
} else if p.X > bounds.Max.X {
bounds.Max.X = p.X
}
if p.Y < bounds.Min.Y {
bounds.Min.Y = p.Y
} else if p.Y > bounds.Max.Y {
bounds.Max.Y = p.Y
}
})
if bounds == inf {
bounds = image.Rectangle{}
}
return bounds
}

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("000000%%%%000000")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("000AB000AB000\x83\x83\x83\x83\x83\x830000aGGGG0aA0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("C7\xea\xa10B00B10000120")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("00000009b00000J0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("00000\x0008--a\xff\x7f0\x00\x00")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("110#\xbd0800000x1ia0\xb6")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("000AB000AB000\x83\x831\x83\x83\x830000aG\x7f\xffG0aA0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("000AA0A\\\\00A0A0A")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0 00. M \xe7\xe7\xe7\xe7\xe77\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0 00. M \xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("!\xff11AB7 0020A0A0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0a000000000\x00\xaaa00")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte(">\xfb:)(\xa9\xdcNS\x81Po\x8f\xc2I\xd4\xd8\x1b\xca\xc7bbbbbb\xfa\x16\xf2\x95\x95c")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xf29ȶ(BBa\xf7007AA07")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xf29ȶ(BBaw007A0\x00\x10")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("000AA0A\\\\20A0\x1e0\x1e0\"")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xf29(\xb6#Bb01007A01a")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("000AB000AB000\x83\x83\x83\x83\x83\x830000a00000aA0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0, 1000001aI21aC2 b1000100bA1")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0y:\x19yd:Y0\x7f8888888")

126
font/bitmap/bitmap.go Normal file
View File

@ -0,0 +1,126 @@
package bitmap
import (
"encoding/binary"
"unicode"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"github.com/mineracks/seedhammer-v1-companion/image/alpha4"
)
type Face struct {
data []byte
}
func NewFace(data []byte) *Face {
return &Face{data}
}
type Kern struct {
R1, R2 uint8
Kern fixed.Int26_6
}
type Glyph struct {
Advance fixed.Int26_6
ImageOff uint16
Rect alpha4.Rectangle
}
const (
indexLen = unicode.MaxASCII
indexElemSize = 4 + 2 + 4
KernElemSize = 2*1 + 4
)
const (
offAscent = 0
offDescent = offAscent + 4
offHeight = offDescent + 4
offIndex = offHeight + 4
offNumKerns = offIndex + indexLen*indexElemSize
OffKerns = offNumKerns + 2
offIndexAdvance = 0
offIndexImageOff = offIndexAdvance + 4
offIndexBounds = offIndexImageOff + 2
)
var bo = binary.LittleEndian
func (f *Face) Metrics() font.Metrics {
return font.Metrics{
Ascent: fixed.Int26_6(bo.Uint32(f.data[offAscent:])),
Descent: fixed.Int26_6(bo.Uint32(f.data[offDescent:])),
Height: fixed.Int26_6(bo.Uint32(f.data[offHeight:])),
}
}
func (f *Face) glyphFor(r rune) (Glyph, bool) {
if r < 0 || int(r) >= indexLen {
return Glyph{}, false
}
index := f.data[offIndex:offNumKerns]
g := index[r*indexElemSize : (r+1)*indexElemSize]
return Glyph{
Advance: fixed.Int26_6(bo.Uint32(g[offIndexAdvance:])),
ImageOff: bo.Uint16(g[offIndexImageOff:]),
Rect: alpha4.Rectangle{
MinX: int8(g[offIndexBounds+0]),
MinY: int8(g[offIndexBounds+1]),
MaxX: int8(g[offIndexBounds+2]),
MaxY: int8(g[offIndexBounds+3]),
},
}, true
}
func (f *Face) GlyphAdvance(r rune) (fixed.Int26_6, bool) {
g, ok := f.glyphFor(r)
if !ok {
return 0, false
}
return g.Advance, true
}
func (f *Face) Kern(r1, r2 rune) fixed.Int26_6 {
cmp := func(kerns []byte, r1, r2 rune, i int) int {
kr1, kr2 := rune(kerns[i*KernElemSize+0]), rune(kerns[i*KernElemSize+1])
if d := int(r1 - kr1); d != 0 {
return d
}
return int(r2 - kr2)
}
nkerns := bo.Uint16(f.data[offNumKerns:])
kerns := f.data[OffKerns : OffKerns+nkerns*KernElemSize]
// Inline sort.Find because TinyGo allocates the closure variables
// despite the closure not escaping.
i, j := 0, int(nkerns)
for i < j {
h := int(uint(i+j) >> 1)
if cmp(kerns, r1, r2, h) > 0 {
i = h + 1
} else {
j = h
}
}
i, found := i, i < int(nkerns) && cmp(kerns, r1, r2, i) == 0
if !found {
return 0
}
return fixed.Int26_6(bo.Uint32(kerns[i*KernElemSize+2:]))
}
func (f *Face) Glyph(r rune) (alpha4.Image, fixed.Int26_6, bool) {
g, ok := f.glyphFor(r)
if !ok {
return alpha4.Image{}, 0, false
}
start := int(g.ImageOff)
bounds := g.Rect.Rect()
npixels := bounds.Dx() * bounds.Dy()
return alpha4.Image{
Pix: f.data[start : start+(npixels+1)/2],
Rect: g.Rect,
}, g.Advance, true
}

221
font/bitmap/convert.go Normal file
View File

@ -0,0 +1,221 @@
//go:build ignore
// generator converts an OpenType file into a bitmap font.
package main
import (
"bytes"
"encoding/binary"
"errors"
"flag"
"fmt"
"go/format"
"image"
"image/color"
"image/draw"
"os"
"path/filepath"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
simage "github.com/mineracks/seedhammer-v1-companion/image"
"github.com/mineracks/seedhammer-v1-companion/image/alpha4"
)
var (
packageName = flag.String("package", "main", "package name")
ppem = flag.Int("ppem", 16, "pixels per em")
alphabet = flag.String("alphabet", "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", "alphabet to generate")
)
type Face struct {
Metrics font.Metrics
// Index maps an ASCII rune to its glyph.
Index [unicode.MaxASCII]bitmap.Glyph
Kerns []bitmap.Kern
Pixels []byte
}
func main() {
flag.Parse()
if flag.NArg() != 2 {
fmt.Fprintf(os.Stderr, "usage: convert infile outfile\n")
os.Exit(1)
}
infile := flag.Arg(0)
ttf, err := os.ReadFile(infile)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
conv, err := parse(ttf, *ppem)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse %q: %v\n", infile, err)
os.Exit(1)
}
if err := generate(filepath.Base(infile), conv); err != nil {
fmt.Fprintf(os.Stderr, "failed to generate %q: %v\n", infile, err)
os.Exit(1)
}
}
func generate(fname string, conv *Face) error {
outname := flag.Arg(1)
var output bytes.Buffer
ext := filepath.Ext(fname)
fname = fname[:len(fname)-len(ext)]
datafile := fmt.Sprintf("%s%d.bin", outname, *ppem)
first, n := utf8.DecodeRuneInString(outname)
name := string(unicode.ToTitle(first)) + outname[n:]
fmt.Fprintf(&output, "// Code generated by font/bitmap/convert.go; DO NOT EDIT.\n\npackage %s\n\n", *packageName)
fmt.Fprintf(&output, "import (\n")
fmt.Fprintf(&output, " _ \"embed\"\n")
fmt.Fprintf(&output, " \"unsafe\"\n")
fmt.Fprintf(&output, " \"github.com/mineracks/seedhammer-v1-companion/font/bitmap\"\n")
fmt.Fprintf(&output, ")\n\n")
fmt.Fprintf(&output, "var %[1]s%[2]d = bitmap.NewFace(unsafe.Slice(unsafe.StringData(data%[3]s%[2]d), len(data%[3]s%[2]d)))\n", name, *ppem, outname)
fmt.Fprintf(&output, "//go:embed %s\n", datafile)
fmt.Fprintf(&output, "var data%s%d string\n", outname, *ppem)
var data []byte
bo := binary.LittleEndian
data = bo.AppendUint32(data, uint32(conv.Metrics.Ascent))
data = bo.AppendUint32(data, uint32(conv.Metrics.Descent))
data = bo.AppendUint32(data, uint32(conv.Metrics.Height))
nkerns := uint16(len(conv.Kerns))
if int(nkerns) != len(conv.Kerns) {
return errors.New("kern table overflows uint16")
}
pixelStart := bitmap.OffKerns + len(conv.Kerns)*bitmap.KernElemSize
for _, g := range conv.Index {
data = bo.AppendUint32(data, uint32(g.Advance))
imgOff := int(g.ImageOff) + pixelStart
off16 := uint16(imgOff)
if int(off16) != imgOff {
return errors.New("pixel offset overflows uint16")
}
data = bo.AppendUint16(data, off16)
data = append(data, uint8(g.Rect.MinX), uint8(g.Rect.MinY), uint8(g.Rect.MaxX), uint8(g.Rect.MaxY))
}
data = bo.AppendUint16(data, nkerns)
for _, k := range conv.Kerns {
data = append(data, k.R1, k.R2)
data = bo.AppendUint32(data, uint32(k.Kern))
}
if len(data) != pixelStart {
panic("pixel start offset miscalculated")
}
data = append(data, conv.Pixels...)
formatted, err := format.Source(output.Bytes())
if err != nil {
fmt.Fprintf(os.Stderr, "failed to format output: %v\n", err)
os.Exit(2)
}
gofile := fmt.Sprintf("%s%d.go", outname, *ppem)
if err := os.WriteFile(gofile, formatted, 0o600); err != nil {
return err
}
return os.WriteFile(datafile, data, 0o600)
}
func face(ttf []byte, ppem int) (font.Face, error) {
f, err := opentype.Parse(ttf)
if err != nil {
return nil, err
}
face, err := opentype.NewFace(f, &opentype.FaceOptions{
Size: float64(ppem),
DPI: 72, // Size is in pixels.
Hinting: font.HintingFull,
})
if err != nil {
return nil, err
}
return face, nil
}
func parse(ttf []byte, ppem int) (*Face, error) {
f, err := face(ttf, ppem)
if err != nil {
return nil, err
}
m := f.Metrics()
face := &Face{
Metrics: font.Metrics{
Ascent: m.Ascent,
Descent: m.Descent,
Height: m.Height,
},
}
// Add whitespaces to alphabet.
var b strings.Builder
for _, r := range *alphabet {
i := uint8(r)
if rune(i) != r {
return nil, errors.New("alphabet overflows uint8")
}
b.WriteByte(i)
}
for i := uint8(0); i < unicode.MaxASCII; i++ {
if unicode.IsSpace(rune(i)) {
b.WriteByte(i)
}
}
alph := b.String()
for i := range face.Index {
r := rune(i)
off := uint16(len(face.Pixels))
if int(off) != len(face.Pixels) {
return nil, errors.New("pixel offset overflows uint16")
}
face.Index[r].ImageOff = off
if strings.IndexRune(alph, r) == -1 {
continue
}
// Brute force n² pairs to construct kerning table. It works
// as long as alphabets are small.
for _, r2 := range alph {
if strings.IndexRune(alph, r2) == -1 {
continue
}
k := f.Kern(r, r2)
if k == 0 {
continue
}
face.Kerns = append(face.Kerns, bitmap.Kern{R1: uint8(r), R2: uint8(r2), Kern: k})
}
dr, mask, maskp, adv, ok := f.Glyph(fixed.P(0, 0), r)
if !ok {
continue
}
alpha, ok := mask.(*image.Alpha)
if !ok {
return nil, fmt.Errorf("bitmap image type %T is not supported", mask)
}
alpha.Rect = dr
rcrop := simage.Crop(alpha)
crop := alpha4.New(alpha4.Rectangle{
MinX: int8(rcrop.Min.X),
MinY: int8(rcrop.Min.Y),
MaxX: int8(rcrop.Max.X),
MaxY: int8(rcrop.Max.Y),
})
if crop.Bounds() != rcrop {
return nil, errors.New("glyph bounds overflows int8")
}
draw.DrawMask(crop, crop.Bounds(), image.NewUniform(color.Black), image.Point{}, alpha, maskp.Add(crop.Bounds().Min), draw.Src)
face.Index[r].Advance = adv
face.Index[r].Rect = crop.Rect
face.Pixels = append(face.Pixels, crop.Pix...)
}
return face, nil
}

Binary file not shown.

Binary file not shown.

93
font/comfortaa/LICENSE Normal file
View File

@ -0,0 +1,93 @@
Copyright 2011 The Comfortaa Project Authors (https://github.com/alexeiva/comfortaa), with Reserved Font Name "Comfortaa".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

BIN
font/comfortaa/bold17.bin Normal file

Binary file not shown.

14
font/comfortaa/bold17.go Normal file
View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package comfortaa
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Bold17 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(databold17), len(databold17)))
//go:embed bold17.bin
var databold17 string

4
font/comfortaa/gen.go Normal file
View File

@ -0,0 +1,4 @@
package comfortaa
//go:generate go run ../bitmap/convert.go -package comfortaa -ppem 17 Comfortaa-Bold.ttf bold
//go:generate go run ../bitmap/convert.go -package comfortaa -ppem 16 Comfortaa-Regular.ttf regular

Binary file not shown.

View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package comfortaa
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Regular16 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(dataregular16), len(dataregular16)))
//go:embed regular16.bin
var dataregular16 string

BIN
font/constant/constant.bin Normal file

Binary file not shown.

13
font/constant/constant.go Normal file
View File

@ -0,0 +1,13 @@
// Code generated by font/vector/convert.go; DO NOT EDIT.
package constant
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/vector"
"unsafe"
)
var Font = vector.NewFace(unsafe.Slice(unsafe.StringData(constantData), len(constantData)))
//go:embed constant.bin
var constantData string

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="SEEDHAMMER_Constant_Font" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 306 9" style="enable-background:new 0 0 306 9;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:0.1;stroke-miterlimit:10;}
</style>
<polyline id="A" class="st0" points="1,5 5,5 5,8 5,3 4,2 2,2 1,3 1,8 "/>
<polyline id="B" class="st0" points="8,5 10,5 11,6 11,7 10,8 7,8 8,8 8,2 7,2 10,2 11,3 11,4 10,5 "/>
<polyline id="C" class="st0" points="17,3 16,2 14,2 13,3 13,7 14,8 16,8 17,7 "/>
<polyline id="D" class="st0" points="19,5 19,2 21,2 23,4 23,6 21,8 19,8 19,5 "/>
<polyline id="E" class="st0" points="25,2 29,2 25,2 25,5 28,5 25,5 25,8 29,8 "/>
<polyline id="F" class="st0" points="31,2 35,2 31,2 31,5 34,5 31,5 31,8 "/>
<polyline id="G" class="st0" points="41,5 39,5 41,5 41,7 40,8 38,8 37,7 37,3 38,2 40,2 41,3 "/>
<polyline id="H" class="st0" points="43,2 43,8 43,5 47,5 47,2 47,8 "/>
<polyline id="I" class="st0" points="51,2 50,2 52,2 51,2 51,8 50,8 52,8 "/>
<polyline id="J" class="st0" points="59,5 59,7 58,8 56,8 55,7 56,8 58,8 59,7 59,2 55,2 "/>
<polyline id="K" class="st0" points="61,2 61,8 61,5 62,5 65,2 62,5 65,8 "/>
<polyline id="L" class="st0" points="67,2 67,8 71,8 "/>
<polyline id="M" class="st0" points="73,8 73,2 75,6 77,2 77,8 "/>
<polyline id="N" class="st0" points="79,8 79,2 83,8 83,2 "/>
<polyline id="O" class="st0" points="85,5 85,3 86,2 88,2 89,3 89,7 88,8 86,8 85,7 85,5 "/>
<polyline id="P" class="st0" points="91,5 94,5 95,4 95,3 94,2 91,2 91,8 "/>
<polyline id="Q" class="st0" points="97,5 97,3 98,2 100,2 101,3 101,7 100,8 99,5 100,8 98,8 97,7 97,5 "/>
<polyline id="R" class="st0" points="103,5 103,2 106,2 107,3 107,4 106,5 105,5 107,8 105,5 103,5 103,8 "/>
<polyline id="S" class="st0" points="109,3 110,2 112,2 113,3 112,2 110,2 109,3 109,4 110,5 112,5 113,6 113,7 112,8 110,8 109,7 "/>
<polyline id="T" class="st0" points="117,2 115,2 119,2 117,2 117,8 "/>
<polyline id="U" class="st0" points="121,2 121,7 122,8 124,8 125,7 125,2 "/>
<polyline id="V" class="st0" points="127,2 129,8 131,2 "/>
<polyline id="W" class="st0" points="133,2 134,8 135,4 136,8 137,2 "/>
<polyline id="X" class="st0" points="139,2 141,5 139,8 141,5 143,8 141,5 143,2 "/>
<polyline id="Y" class="st0" points="145,2 147,5 147,8 147,5 149,2 "/>
<polyline id="Z" class="st0" points="151,2 155,2 151,8 155,8 "/>
<polyline id="one" class="st0" points="159,2 157,3 159,2 159,8 157,8 161,8 "/>
<polyline id="two" class="st0" points="163,4 163,3 164,2 166,2 167,3 167,4 163,8 167,8 "/>
<polyline id="three" class="st0" points="169,3 170,2 172,2 173,3 173,4 172,5 171,5 172,5 173,6 173,7 172,8 170,8 169,7 "/>
<polyline id="four" class="st0" points="178,8 178,2 175,6 179,6 "/>
<polyline id="five" class="st0" points="185,2 181,2 181,5 184,5 185,6 185,7 184,8 182,8 181,7 "/>
<polyline id="six" class="st0" points="191,3 190,2 188,2 187,3 187,5 187,7 188,8 190,8 191,7 191,6 190,5 188,5 187,6 "/>
<polyline id="seven" class="st0" points="193,2 197,2 194,8 "/>
<polyline id="eight" class="st0" points="202,5 200,5 199,6 199,7 200,8 202,8 203,7 203,6 202,5 203,4 203,3 202,2 200,2 199,3 199,4 200,5 "/>
<polyline id="nine" class="st0" points="208,5 206,5 205,4 205,3 206,2 208,2 209,3 209,4 206,8 "/>
<polyline id="zero" class="st0" points="211,6 211,7 215,3 214,2 212,2 211,3 211,7 212,8 214,8 215,7 215,3 "/>
<g id="colon">
<polyline class="st0" points="218,3 219,3 219,4 218,4 218,3 "/>
<polyline class="st0" points="218,7 219,7 219,8 218,8 218,7 "/>
</g>
<line id="comma" class="st0" x1="225" y1="7" x2="224" y2="8"/>
<line id="slash" class="st0" x1="232" y1="2" x2="230" y2="8"/>
<line id="apostrophe" class="st0" x1="237" y1="2" x2="236" y2="4"/>
<polyline id="period" class="st0" points="242,7 243,7 243,8 242,8 242,7 "/>
<polyline id="leftparen" class="st0" points="250,2 249,4 249,6 250,8 "/>
<polyline id="rightparen" class="st0" points="254,2 255,4 255,6 254,8 "/>
<polyline id="leftbracket" class="st0" points="261,2 262,2 261,2 261,8 262,8 "/>
<polyline id="rightbracket" class="st0" points="267,2 266,2 267,2 267,8 266,8 "/>
<polyline id="leftcurlybrace" class="st0" points="275,8 274,8 273,7 273,6 272,5 273,4 273,4 273,4 273,3 274,2 275,2 "/>
<polyline id="rightcurlybrace" class="st0" points="277,2 278,2 279,3 279,4 280,5 279,6 279,6 279,6 279,7 278,8 277,8 "/>
<g id="hash">
<line class="st0" x1="284" y1="2" x2="284" y2="8"/>
<line class="st0" x1="286" y1="2" x2="286" y2="8"/>
<line class="st0" x1="283" y1="4" x2="287" y2="4"/>
<line class="st0" x1="283" y1="6" x2="287" y2="6"/>
</g>
<g id="star">
<line class="st0" x1="291" y1="2" x2="291" y2="4"/>
<line class="st0" x1="290" y1="2" x2="292" y2="4"/>
<line class="st0" x1="290" y1="3" x2="292" y2="3"/>
<line class="st0" x1="290" y1="4" x2="292" y2="2"/>
</g>
<polyline id="at" class="st0" points="299,7 298,8 296,8 295,7 295,4 295,3 296,2 298,2 299,3 299,6 298,5 298,4 297,3 296,4 296,6 297,7 298,6 "/>
<line id="dash" class="st0" x1="301" y1="5" x2="305" y2="5"/>
<line id="height" class="st0" x1="6" y1="0" x2="6" y2="9"/>
<line id="advance" class="st0" x1="0" y1="8" x2="6" y2="8"/>
<line id="baseline" class="st0" x1="2" y1="8" x2="4" y2="8"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

5
font/constant/gen.go Normal file
View File

@ -0,0 +1,5 @@
// package constant contains the font used for engraving constant-time seed
// words.
package constant
//go:generate go run ../vector/convert.go -package constant constant.svg constant

93
font/poppins/LICENSE Normal file
View File

@ -0,0 +1,93 @@
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

BIN
font/poppins/bold10.bin Normal file

Binary file not shown.

14
font/poppins/bold10.go Normal file
View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package poppins
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Bold10 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(databold10), len(databold10)))
//go:embed bold10.bin
var databold10 string

BIN
font/poppins/bold16.bin Normal file

Binary file not shown.

14
font/poppins/bold16.go Normal file
View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package poppins
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Bold16 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(databold16), len(databold16)))
//go:embed bold16.bin
var databold16 string

BIN
font/poppins/bold20.bin Normal file

Binary file not shown.

14
font/poppins/bold20.go Normal file
View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package poppins
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Bold20 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(databold20), len(databold20)))
//go:embed bold20.bin
var databold20 string

BIN
font/poppins/bold23.bin Normal file

Binary file not shown.

14
font/poppins/bold23.go Normal file
View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package poppins
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Bold23 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(databold23), len(databold23)))
//go:embed bold23.bin
var databold23 string

Binary file not shown.

View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package poppins
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Boldprogress45 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(databoldprogress45), len(databoldprogress45)))
//go:embed boldprogress45.bin
var databoldprogress45 string

10
font/poppins/gen.go Normal file
View File

@ -0,0 +1,10 @@
package poppins
//go:generate go run ../bitmap/convert.go -package poppins -ppem 16 Poppins-Regular.ttf regular
//go:generate go run ../bitmap/convert.go -package poppins -ppem 10 Poppins-Bold.ttf bold
//go:generate go run ../bitmap/convert.go -package poppins -ppem 16 Poppins-Bold.ttf bold
//go:generate go run ../bitmap/convert.go -package poppins -ppem 20 Poppins-Bold.ttf bold
//go:generate go run ../bitmap/convert.go -package poppins -ppem 23 Poppins-Bold.ttf bold
// Size 45 is only for progress indicators
//go:generate go run ../bitmap/convert.go -package poppins -ppem 45 -alphabet "0123456789%" Poppins-Bold.ttf boldprogress

BIN
font/poppins/regular16.bin Normal file

Binary file not shown.

14
font/poppins/regular16.go Normal file
View File

@ -0,0 +1,14 @@
// Code generated by font/bitmap/convert.go; DO NOT EDIT.
package poppins
import (
_ "embed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
"unsafe"
)
var Regular16 = bitmap.NewFace(unsafe.Slice(unsafe.StringData(dataregular16), len(dataregular16)))
//go:embed regular16.bin
var dataregular16 string

490
font/vector/convert.go Normal file
View File

@ -0,0 +1,490 @@
//go:build ignore
package main
import (
"bytes"
"encoding/binary"
"encoding/xml"
"errors"
"flag"
"fmt"
"go/format"
"image"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"unicode"
"github.com/mineracks/seedhammer-v1-companion/font/vector"
)
var packageName = flag.String("package", "main", "package name")
type Face struct {
Metrics vector.Metrics
// Index maps a character to its segment range.
Index [unicode.MaxASCII]vector.Glyph
// Segments encoded as opcode, args, opcode, args...
Segments []byte
}
func main() {
flag.Parse()
if flag.NArg() != 2 {
fmt.Fprintf(os.Stderr, "usage: convert infile outfile\n")
os.Exit(1)
}
infile := flag.Arg(0)
in, err := os.ReadFile(infile)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
conv, err := convert(in)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse %q: %v\n", infile, err)
os.Exit(1)
}
if err := generate(filepath.Base(infile), conv); err != nil {
fmt.Fprintf(os.Stderr, "failed to generate %q: %v\n", infile, err)
os.Exit(1)
}
}
func generate(fname string, conv *Face) error {
var output bytes.Buffer
ext := filepath.Ext(fname)
fname = fname[:len(fname)-len(ext)]
name := flag.Arg(1)
fmt.Fprintf(&output, "// Code generated by font/vector/convert.go; DO NOT EDIT.\npackage %s\n", *packageName)
fmt.Fprintf(&output, "import (\n")
fmt.Fprintf(&output, " _ \"embed\"\n")
fmt.Fprintf(&output, " \"unsafe\"\n")
fmt.Fprintf(&output, " \"github.com/mineracks/seedhammer-v1-companion/font/vector\"\n")
fmt.Fprintf(&output, ")\n\n")
fmt.Fprintf(&output, "var Font = vector.NewFace(unsafe.Slice(unsafe.StringData(%sData), len(%[1]sData)))\n\n", name)
fmt.Fprintf(&output, "//go:embed %s.bin\n", name)
fmt.Fprintf(&output, "var %sData string\n", name)
formatted, err := format.Source(output.Bytes())
if err != nil {
return err
}
var data []byte
bo := binary.LittleEndian
data = append(data, uint8(conv.Metrics.Ascent), uint8(conv.Metrics.Height))
for _, g := range conv.Index {
data = append(data, uint8(g.Advance))
start, end := int(g.Start)+vector.OffSegments, int(g.End)+vector.OffSegments
s16, e16 := uint16(start), uint16(end)
if int(s16) != start || int(e16) != end {
return errors.New("segment offset overflows uint16")
}
data = bo.AppendUint16(data, s16)
data = bo.AppendUint16(data, e16)
}
if len(data) != vector.OffSegments {
panic("miscalculated segment offset")
}
data = append(data, conv.Segments...)
if err := os.WriteFile(name+".go", formatted, 0o600); err != nil {
return err
}
return os.WriteFile(name+".bin", data, 0o600)
}
type MetaData struct {
Advance, Height, Baseline int
}
func convert(svg []byte) (*Face, error) {
d := xml.NewDecoder(bytes.NewReader(svg))
for {
root, err := d.Token()
if err != nil {
return nil, err
}
t, ok := root.(xml.StartElement)
if !ok {
continue
}
if !ok || t.Name.Local != "svg" {
return nil, errors.New("missing <svg> root element")
}
face := new(Face)
meta, err := parseMeta(svg)
if err != nil {
return nil, err
}
ascent := int8(meta.Baseline)
if int(ascent) != meta.Baseline {
return nil, errors.New("baseline overflows int8")
}
height := int8(meta.Height)
if int(height) != meta.Height {
return nil, errors.New("height overlflows int8")
}
face.Metrics.Ascent = ascent
face.Metrics.Height = height
adv := meta.Advance
face.Index[' '] = vector.Glyph{
Advance: int8(adv),
}
err = parseChars(face, d, adv, int(ascent))
return face, err
}
}
func parseMeta(data []byte) (*MetaData, error) {
type Line struct {
ID string `xml:"id,attr"`
X1 float64 `xml:"x1,attr"`
Y1 float64 `xml:"y1,attr"`
X2 float64 `xml:"x2,attr"`
Y2 float64 `xml:"y2,attr"`
}
type SVG struct {
XMLName xml.Name `xml:"svg"`
Lines []Line `xml:"line"`
}
var svg SVG
if err := xml.Unmarshal(data, &svg); err != nil {
return nil, err
}
var meta MetaData
for _, line := range svg.Lines {
switch line.ID {
case "advance":
meta.Advance = mustInt(line.X2 - line.X1)
case "height":
meta.Height = mustInt(line.Y2 - line.Y1)
case "baseline":
meta.Baseline = mustInt(line.Y1)
}
}
return &meta, nil
}
func findAttr(e xml.StartElement, name string) (string, bool) {
for _, a := range e.Attr {
if a.Name.Local == name {
return a.Value, true
}
}
return "", false
}
func mustInt(v float64) int {
i := int(v)
if float64(i) != v {
panic("non-integer floating point number")
}
return i
}
func parseChars(face *Face, d *xml.Decoder, adv, ascent int) error {
offx := 0
for {
t, err := d.Token()
if err != nil {
if err != io.EOF {
return err
}
break
}
e, ok := t.(xml.StartElement)
if !ok {
continue
}
switch e.Name.Local {
case "style":
if err := d.Skip(); err != nil {
return err
}
continue
}
id, _ := findAttr(e, "id")
switch id {
case "advance", "height", "baseline", "size":
// Skip anonymous and meta-data elements.
if err := d.Skip(); err != nil {
return err
}
continue
}
r, ok := mapChar(id)
if !ok {
return fmt.Errorf("unknown character id: %q", id)
}
idxStart := len(face.Segments)
if err := parseSegments(face, d, e, offx, -ascent); err != nil {
return err
}
idxEnd := len(face.Segments)
face.Index[r] = vector.Glyph{
Advance: int8(adv),
Start: uint16(idxStart),
End: uint16(idxEnd),
}
offx -= adv
}
return nil
}
func parseSegments(face *Face, d *xml.Decoder, e xml.StartElement, offx, offy int) error {
encode := func(op vector.SegmentOp, args ...image.Point) {
face.Segments = append(face.Segments, byte(op))
for _, a := range args {
x, y := int8(a.X), int8(a.Y)
if int(x) != a.X || int(y) != a.Y {
panic(fmt.Errorf("coordinates out of range: %v", a))
}
face.Segments = append(face.Segments, byte(x), byte(y))
}
}
switch n := e.Name.Local; n {
case "g":
for {
t, err := d.Token()
if err != nil {
return err
}
switch t := t.(type) {
case xml.StartElement:
if err := parseSegments(face, d, t, offx, offy); err != nil {
return err
}
case xml.EndElement:
return nil
}
}
case "line":
var line struct {
X1 float64 `xml:"x1,attr"`
Y1 float64 `xml:"y1,attr"`
X2 float64 `xml:"x2,attr"`
Y2 float64 `xml:"y2,attr"`
}
if err := d.DecodeElement(&line, &e); err != nil {
return err
}
encode(vector.SegmentOpMoveTo, image.Pt(mustInt(line.X1)+offx, mustInt(line.Y1)+offy))
encode(vector.SegmentOpLineTo, image.Pt(mustInt(line.X2)+offx, mustInt(line.Y2)+offy))
return nil
case "polyline":
points, ok := findAttr(e, "points")
if !ok {
return errors.New("missing points attribute for <polyline>")
}
points = strings.TrimSpace(points)
coords := strings.Split(points, " ")
for i, c := range coords {
var x, y float64
if _, err := fmt.Sscanf(c, "%f,%f", &x, &y); err != nil {
return fmt.Errorf("invalid coordinates %q in <polyline>:", c)
}
op := vector.SegmentOpLineTo
if i == 0 {
op = vector.SegmentOpMoveTo
}
encode(op, image.Pt(mustInt(x)+offx, mustInt(y)+offy))
}
return d.Skip()
case "path":
cmds, ok := findAttr(e, "d")
if !ok {
return errors.New("missing d attribute for <path>")
}
cmds = strings.TrimSpace(cmds)
pen := image.Pt(offx, offy)
initPoint := pen
ctrl2 := pen
for {
cmds = strings.TrimLeft(cmds, " ,\t\n")
if len(cmds) == 0 {
break
}
orig := cmds
op := rune(cmds[0])
cmds = cmds[1:]
switch op {
case 'M', 'm', 'V', 'v', 'L', 'l', 'H', 'h', 'C', 'c', 'S', 's':
case 'Z', 'z':
if pen != initPoint {
encode(vector.SegmentOpLineTo, initPoint)
pen = initPoint
}
ctrl2 = initPoint
continue
default:
return fmt.Errorf("unknown <path> command %s in %q", string(op), orig)
}
var coords []int
for {
cmds = strings.TrimLeft(cmds, " ,\t\n")
if len(cmds) == 0 {
break
}
n, x, ok := parseFloat(cmds)
if !ok {
break
}
cmds = cmds[n:]
coords = append(coords, mustInt(x))
}
rel := unicode.IsLower(op)
newPen := pen
switch unicode.ToLower(op) {
case 'h':
for _, x := range coords {
p := image.Pt(x, pen.Y)
if rel {
p.X += pen.X
} else {
p.X += offx
}
encode(vector.SegmentOpLineTo, p)
newPen = p
}
pen = newPen
ctrl2 = newPen
continue
case 'v':
for _, y := range coords {
p := image.Pt(pen.X, y)
if rel {
p.Y += pen.Y
} else {
p.Y += offy
}
encode(vector.SegmentOpLineTo, p)
newPen = p
}
pen = newPen
ctrl2 = newPen
continue
}
if len(coords)%2 != 0 {
return fmt.Errorf("odd number of coordinates in <path> data: %q", orig)
}
var off image.Point
if rel {
// Relative command.
off = pen
} else {
off = image.Pt(offx, offy)
}
var points []image.Point
for i := 0; i < len(coords); i += 2 {
p := image.Pt(coords[i], coords[i+1])
p = p.Add(off)
points = append(points, p)
}
newCtrl2 := ctrl2
switch op := unicode.ToLower(op); op {
case 'm', 'l':
sop := vector.SegmentOpMoveTo
if op == 'l' {
sop = vector.SegmentOpLineTo
}
for _, p := range points {
encode(sop, p)
newPen = p
}
if op == 'm' {
initPoint = newPen
}
case 'c', 's':
return errors.New("cubic splines not supported")
}
pen = newPen
ctrl2 = newCtrl2
}
return d.Skip()
default:
return fmt.Errorf("unsupported element: <%s>", n)
}
}
func parseFloat(s string) (int, float64, bool) {
n := 0
if len(s) > 0 && s[0] == '-' {
n++
}
for ; n < len(s); n++ {
if !(unicode.IsDigit(rune(s[n])) || s[n] == '.') {
break
}
}
f, err := strconv.ParseFloat(s[:n], 64)
return n, f, err == nil
}
func mapChar(id string) (rune, bool) {
var r rune
switch {
case len(id) == 1:
r = rune(id[0])
default:
switch id {
case "zero":
r = '0'
case "one":
r = '1'
case "two":
r = '2'
case "three":
r = '3'
case "four":
r = '4'
case "five":
r = '5'
case "six":
r = '6'
case "seven":
r = '7'
case "eight":
r = '8'
case "nine":
r = '9'
case "colon":
r = ':'
case "comma":
r = ','
case "slash":
r = '/'
case "apostrophe":
r = '\''
case "dash":
r = '-'
case "period":
r = '.'
case "leftparen":
r = '('
case "rightparen":
r = ')'
case "leftbracket":
r = '['
case "rightbracket":
r = ']'
case "leftcurlybrace":
r = '{'
case "rightcurlybrace":
r = '}'
case "hash":
r = '#'
case "star":
r = '*'
case "at":
r = '@'
default:
return 0, false
}
}
return r, true
}

92
font/vector/font.go Normal file
View File

@ -0,0 +1,92 @@
// package font converts an OpenType font into a form usable for engraving.
package vector
import (
"encoding/binary"
"image"
"unicode"
)
type Face struct {
data []byte
}
func NewFace(data []byte) *Face {
return &Face{data}
}
type Glyph struct {
Advance int8
Start, End uint16
}
// Segments is an iterator over a glyph's segments
type Segments struct {
segs []byte
}
func (s *Segments) Next() (Segment, bool) {
if len(s.segs) == 0 {
return Segment{}, false
}
seg := Segment{
Op: SegmentOp(s.segs[0]),
Arg: image.Point{
X: int(int8(s.segs[1])),
Y: int(int8(s.segs[2])),
},
}
s.segs = s.segs[3:]
return seg, true
}
// Segment is like sfnt.Segment but with integer coordinates.
type Segment struct {
Op SegmentOp
Arg image.Point
}
type Metrics struct {
Ascent, Height int8
}
type SegmentOp uint32
const (
SegmentOpMoveTo SegmentOp = iota
SegmentOpLineTo
)
const (
indexLen = unicode.MaxASCII
IndexElemSize = 1 + 2 + 2
offAscent = 0
offHeight = offAscent + 1
offIndex = offHeight + 1
OffSegments = offIndex + indexLen*IndexElemSize
)
var bo = binary.LittleEndian
func (f *Face) Metrics() Metrics {
return Metrics{
Ascent: int8(f.data[offAscent]),
Height: int8(f.data[offHeight]),
}
}
func (f *Face) Decode(ch rune) (int, Segments, bool) {
if int(ch) >= indexLen {
return 0, Segments{}, false
}
index := f.data[offIndex:OffSegments]
gdata := index[ch*IndexElemSize : (ch+1)*IndexElemSize]
g := Glyph{
Advance: int8(gdata[0]),
Start: bo.Uint16(gdata[1:]),
End: bo.Uint16(gdata[1+2:]),
}
segs := f.data[g.Start:g.End]
return int(g.Advance), Segments{segs: segs}, g.Advance > 0
}

26
go.mod
View File

@ -2,4 +2,28 @@ module github.com/mineracks/seedhammer-v1-companion
go 1.24.0 go 1.24.0
require gonum.org/v1/gonum v0.17.0 require (
github.com/btcsuite/btcd v0.23.0
github.com/btcsuite/btcd/btcec/v2 v2.1.3
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/fxamacker/cbor/v2 v2.4.0
github.com/kortschak/qr v0.3.0
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
golang.org/x/crypto v0.7.0
golang.org/x/image v0.25.0
gonum.org/v1/gonum v0.17.0
)
require (
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.6.0 // indirect
)
// Match upstream: their kortschak/qr fork has CBOR + binary-mode tweaks
// not in mainline. Pinned to the same revision upstream uses at v1.3.0.
replace github.com/kortschak/qr => github.com/seedhammer/kortschak-qr v0.0.0-20240113235555-375796488df0

123
go.sum
View File

@ -1,2 +1,125 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA=
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/seedhammer/kortschak-qr v0.0.0-20240113235555-375796488df0 h1:C/GBca2LVCIeBQWOMBgrbcMV70hW2S5gO8aSAgSLJOc=
github.com/seedhammer/kortschak-qr v0.0.0-20240113235555-375796488df0/go.mod h1:l0kMewIexD8HRZ8iW+lFf8y74IvP7652/0wAlaRf22U=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

78
image/alpha4/alpha4.go Normal file
View File

@ -0,0 +1,78 @@
// Package alpha4 implements an [image.Alpha] replacement
// with compact 4-bit alpha values.
package alpha4
import (
"image"
"image/color"
)
type Image struct {
Pix []byte
Rect Rectangle
}
type Rectangle struct {
MinX, MinY, MaxX, MaxY int8
}
func New(r Rectangle) *Image {
npixels := int(r.MaxX-r.MinX) * int(r.MaxY-r.MinY)
return &Image{
Pix: make([]byte, (npixels+1)/2),
Rect: r,
}
}
func (p *Image) ColorModel() color.Model { panic("not implemented") }
func (p *Image) Bounds() image.Rectangle { return p.Rect.Rect() }
func (p *Image) At(x, y int) color.Color {
return p.AlphaAt(x, y)
}
func (p *Image) RGBA64At(x, y int) color.RGBA64 {
a := uint16(p.AlphaAt(x, y).A)
a |= a << 8
return color.RGBA64{a, a, a, a}
}
func (p *Image) AlphaAt(x, y int) color.Alpha {
if !(image.Point{x, y}.In(p.Rect.Rect())) {
return color.Alpha{}
}
i := p.PixOffset(x, y)
a2 := p.Pix[i/2]
return color.Alpha{alpha4(i, a2)}
}
func alpha4(i int, a2 byte) byte {
a := (a2 >> ((^i & 0b1) * 4)) & 0b1111
return a<<4 | a
}
func (p *Image) PixOffset(x, y int) int {
return (y-int(p.Rect.MinY))*int(p.Rect.MaxX-p.Rect.MinX) + (x - int(p.Rect.MinX))
}
func (r Rectangle) Rect() image.Rectangle {
return image.Rect(int(r.MinX), int(r.MinY), int(r.MaxX), int(r.MaxY))
}
func (p *Image) Set(x, y int, c color.Color) {
panic("not implemented")
}
func (p *Image) SetRGBA64(x, y int, c color.RGBA64) {
if !(image.Point{x, y}).In(p.Rect.Rect()) {
return
}
i := p.PixOffset(x, y)
a2 := p.Pix[i/2]
mask := byte(0b1111) << ((i & 0b1) * 4)
a2 &= mask
a := byte(c.A >> 12)
a <<= ((^i & 0b1) * 4)
p.Pix[i/2] = a2 | a
}

View File

@ -0,0 +1,32 @@
package alpha4
import (
"bytes"
"image"
"image/color"
"image/draw"
"testing"
)
func TestExhausting(t *testing.T) {
r := Rectangle{10, 10, 12, 12}
img1 := image.NewAlpha(r.Rect())
a4 := New(r)
img2 := image.NewAlpha(img1.Rect)
for a1 := byte(0); a1 <= 0b1111; a1++ {
for a2 := byte(0); a2 <= 0b1111; a2++ {
for a3 := byte(0); a3 <= 0b1111; a3++ {
img1.SetAlpha(10, 10, color.Alpha{A: a1<<4 | a1})
img1.SetAlpha(11, 10, color.Alpha{A: a2<<4 | a2})
img1.SetAlpha(10, 11, color.Alpha{A: a3<<4 | a3})
draw.Draw(a4, a4.Bounds(), img1, img1.Bounds().Min, draw.Src)
draw.Draw(img2, img2.Bounds(), a4, a4.Bounds().Min, draw.Src)
if !bytes.Equal(img1.Pix, img2.Pix) {
t.Errorf("%.8b %.8b %.8b roundtripped to %.8b %.8b %.8b",
img1.AlphaAt(10, 10), img1.AlphaAt(11, 10), img1.AlphaAt(10, 11),
img2.AlphaAt(10, 10), img2.AlphaAt(11, 10), img2.AlphaAt(11, 10))
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More