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>
157
address/address.go
Normal 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
@ -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
@ -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
@ -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,
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,18 @@
|
||||
// Package backup defines SeedHammer v1 plate dimensions and layout
|
||||
// constants.
|
||||
// Package backup defines SeedHammer v1 plate dimensions and the backup-
|
||||
// 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)
|
||||
// SquarePlate 85 × 85 mm — single seed + title
|
||||
// LargePlate 85 × 134 mm — seed + descriptor for multisig
|
||||
//
|
||||
// The engraver's origin sits 97mm in the X axis from the plate edge (a
|
||||
// fixed offset of the physical machine). outerMargin = 3 mm and
|
||||
// innerMargin = 10 mm define the engrave-safe area.
|
||||
// LIFTED from upstream seedhammer/seedhammer at v1.3.0
|
||||
// (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e). NOT lifted from
|
||||
// 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
|
||||
// (1 step ≈ 0.00796 mm) at command-generation time.
|
||||
//
|
||||
// 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.
|
||||
// backup_test.go from upstream pulls in mjolnir + engrave + bip32 — we've
|
||||
// renamed it to backup_test.go.deferred until those packages land.
|
||||
package backup
|
||||
|
||||
BIN
backup/testdata/plate-0-side-0-1-of-1-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
backup/testdata/plate-1-side-0-1-of-1-words-24.png
vendored
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
backup/testdata/plate-10-side-0-1-of-1-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
backup/testdata/plate-11-side-0-1-of-2-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backup/testdata/plate-12-side-0-2-of-3-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
backup/testdata/plate-13-side-0-3-of-5-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backup/testdata/plate-14-side-0-9-of-10-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
backup/testdata/plate-2-side-1-1-of-1-words-24.png
vendored
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
backup/testdata/plate-3-side-0-1-of-1-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
backup/testdata/plate-4-side-0-1-of-1-words-24.png
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
backup/testdata/plate-5-side-1-3-of-5-words-24.png
vendored
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
backup/testdata/plate-6-side-1-1-of-1-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
backup/testdata/plate-7-side-1-1-of-1-words-24.png
vendored
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
backup/testdata/plate-8-side-1-1-of-2-words-12.png
vendored
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
backup/testdata/plate-9-side-1-3-of-5-words-24.png
vendored
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
120
bc/bytewords/bytewords.go
Normal 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"
|
||||
62
bc/bytewords/bytewords_test.go
Normal 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
@ -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
@ -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]
|
||||
}
|
||||
}
|
||||
163
bc/fountain/fountain_test.go
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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))
|
||||
}
|
||||
42
bc/xoshiro256/xoshiro_test.go
Normal 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
@ -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
@ -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
@ -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
@ -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
10
bip39/wordlist.go
Normal file
12
driver/mjolnir/doc.go
Normal 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
@ -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
@ -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
@ -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
|
||||
}
|
||||
@ -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
|
||||
// a stream of MoveTo/LineTo commands in the engraver's coordinate
|
||||
// system (machine steps, 1 step ≈ 0.00796 mm).
|
||||
//
|
||||
// - 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.
|
||||
// Subpackages:
|
||||
// - engrave/wire/sh1e/ — the SH1E plate-design envelope (new code, ours)
|
||||
// - engrave/wire/ — the live MarkingWay USB-serial encoder (future)
|
||||
package engrave
|
||||
|
||||
1110
engrave/engrave.go
Executable file
99
engrave/engrave_test.go
Normal 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
|
||||
}
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/0044e99cbc44b9cb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("000000%%%%000000")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/33036c11bb0bfb0e
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("000AB000AB000\x83\x83\x83\x83\x83\x830000aGGGG0aA0")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/33845efe3c537c51
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("C7\xea\xa10B00B10000120")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/4e76ff3f53ac4ba9
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("00000009b00000J0")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/5dc25ba08d2ff621
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("00000\x0008--a\xff\x7f0\x00\x00")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/5e25402fbbde57ed
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("110#\xbd0800000x1ia0\xb6")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/95e9e16c63e5c5aa
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("000AB000AB000\x83\x831\x83\x83\x830000aG\x7f\xffG0aA0")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/9b04e1abfa4a9922
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("000AA0A\\\\00A0A0A")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/aff07eb2917f82aa
vendored
Normal 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")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/b315f2c6941e37be
vendored
Normal 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")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/b68e4917511c2b07
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("!\xff11AB7 0020A0A0")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/bb590f94596b5bb3
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0a000000000\x00\xaaa00")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/c71011d9b4b52f1d
vendored
Normal 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")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/daeec663812e4d5d
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("\xf29ȶ(BBa\xf7007AA07")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/dca96a5694c2e749
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("\xf29ȶ(BBaw007A0\x00\x10")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/dff102b100b3a2d2
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("000AA0A\\\\20A0\x1e0\x1e0\"")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/ee3f9e490c1852e7
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("\xf29(\xb6#Bb01007A01a")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/f0d9c7c3456933e4
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("000AB000AB000\x83\x83\x83\x83\x83\x830000a00000aA0")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/fb39f4e8f0a08b59
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0, 1000001aI21aC2 b1000100bA1")
|
||||
2
engrave/testdata/fuzz/FuzzConstantQR/ff7a9abeb3e23ad7
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0y:\x19yd:Y0\x7f8888888")
|
||||
126
font/bitmap/bitmap.go
Normal 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
@ -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
|
||||
}
|
||||
BIN
font/comfortaa/Comfortaa-Bold.ttf
Normal file
BIN
font/comfortaa/Comfortaa-Regular.ttf
Normal file
93
font/comfortaa/LICENSE
Normal 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
14
font/comfortaa/bold17.go
Normal 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
@ -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
|
||||
BIN
font/comfortaa/regular16.bin
Normal file
14
font/comfortaa/regular16.go
Normal 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
13
font/constant/constant.go
Normal 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
|
||||
75
font/constant/constant.svg
Normal 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
@ -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
@ -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.
|
||||
BIN
font/poppins/Poppins-Bold.ttf
Normal file
BIN
font/poppins/Poppins-Regular.ttf
Normal file
BIN
font/poppins/bold10.bin
Normal file
14
font/poppins/bold10.go
Normal 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
14
font/poppins/bold16.go
Normal 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
14
font/poppins/bold20.go
Normal 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
14
font/poppins/bold23.go
Normal 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
|
||||
BIN
font/poppins/boldprogress45.bin
Normal file
14
font/poppins/boldprogress45.go
Normal 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
@ -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
14
font/poppins/regular16.go
Normal 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
@ -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
@ -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
@ -2,4 +2,28 @@ module github.com/mineracks/seedhammer-v1-companion
|
||||
|
||||
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
@ -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/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
@ -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
|
||||
}
|
||||
32
image/alpha4/alpha4_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||