Initial commit: shared keychain interfaces and implementations

Provides Signer, Keychain, and Ledger interfaces for the Lux ecosystem:
- Signer: Signs hashes and provides address
- Keychain: Manages signers by address
- Ledger: Hardware wallet interface
- LedgerKeychain: Keychain implementation backed by Ledger device

Dependencies:
- github.com/luxfi/ids
- github.com/luxfi/math/set
This commit is contained in:
2025-12-24 16:31:36 -08:00
commit 4916a3edca
4 changed files with 311 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
module github.com/luxfi/keychain
go 1.25.5
require (
github.com/luxfi/ids v1.2.5
github.com/luxfi/math v1.2.0
github.com/stretchr/testify v1.11.1
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/luxfi/crypto v1.17.25 // indirect
github.com/luxfi/sampler v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
gonum.org/v1/gonum v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+36
View File
@@ -0,0 +1,36 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luxfi/crypto v1.17.25 h1:emTejQ17bGj/1tu0ETB196Hbn6fJfo8wUNGIOWit6RM=
github.com/luxfi/crypto v1.17.25/go.mod h1:v6eaW5ejuzW2gecChWKQqx4CtVhoM1b+e40ro/6WVPg=
github.com/luxfi/ids v1.2.5 h1:w4YSj3S6LQiUXhiChUmcbrMCRMfkCaBFIGn0Eo4ZSxk=
github.com/luxfi/ids v1.2.5/go.mod h1:vZBjJWSmqMIZuodgnoszvTDvXIwsbnhM2pkktg6FGrg=
github.com/luxfi/math v1.2.0 h1:cZVQQj1PrIWh3Oy6xfbBL44xay4QFuAXZBg9+pWPBmE=
github.com/luxfi/math v1.2.0/go.mod h1:gcmMy6raVf6xrQW/4UmFXEEXC/UC2tZeGkxdiDzeBSg=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+121
View File
@@ -0,0 +1,121 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package keychain provides shared keychain interfaces and implementations
// for hardware wallets and software signers in the Lux ecosystem.
package keychain
import (
"errors"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
)
var (
ErrInvalidIndicesLength = errors.New("number of indices should be greater than 0")
ErrInvalidNumAddrsToDerive = errors.New("number of addresses to derive should be greater than 0")
ErrInvalidNumAddrsDerived = errors.New("incorrect number of ledger derived addresses")
ErrInvalidNumSignatures = errors.New("incorrect number of signatures")
)
// Signer implements functions for a keychain to return its main address and
// to sign a hash
type Signer interface {
SignHash([]byte) ([]byte, error)
Sign([]byte) ([]byte, error)
Address() ids.ShortID
}
// Keychain maintains a set of addresses together with their corresponding
// signers
type Keychain interface {
// The returned Signer can provide a signature for [addr]
Get(addr ids.ShortID) (Signer, bool)
// Returns the set of addresses for which the accessor keeps an associated
// signer
Addresses() set.Set[ids.ShortID]
}
// Ledger interface for hardware wallet support
type Ledger interface {
Address(displayHRP string, addressIndex uint32) (ids.ShortID, error)
SignHash(hash []byte, addressIndex uint32) ([]byte, error)
Sign(hash []byte, addressIndex uint32) ([]byte, error)
SignTransaction(rawUnsignedHash []byte, addressIndices []uint32) ([][]byte, error)
GetAddresses(addressIndices []uint32) ([]ids.ShortID, error)
Disconnect() error
}
// ledgerKeychain is an abstraction of the underlying ledger hardware device,
// to be able to get a signer from a finite set of derived signers
type ledgerKeychain struct {
ledger Ledger
addrs set.Set[ids.ShortID]
addrToIdx map[ids.ShortID]uint32
}
// ledgerSigner is an abstraction of the underlying ledger hardware device,
// capable of extracting its main address and signing a hash
type ledgerSigner struct {
ledger Ledger
idx uint32
addr ids.ShortID
}
// NewLedgerKeychain creates a new ledger keychain
func NewLedgerKeychain(ledger Ledger, indices []uint32) (Keychain, error) {
if len(indices) == 0 {
return nil, ErrInvalidIndicesLength
}
addresses, err := ledger.GetAddresses(indices)
if err != nil {
return nil, err
}
if len(addresses) != len(indices) {
return nil, ErrInvalidNumAddrsDerived
}
addrToIdx := make(map[ids.ShortID]uint32)
addrs := make(set.Set[ids.ShortID])
for i, addr := range addresses {
addrToIdx[addr] = indices[i]
addrs.Add(addr)
}
return &ledgerKeychain{
ledger: ledger,
addrs: addrs,
addrToIdx: addrToIdx,
}, nil
}
func (l *ledgerKeychain) Get(addr ids.ShortID) (Signer, bool) {
idx, ok := l.addrToIdx[addr]
if !ok {
return nil, false
}
return &ledgerSigner{
ledger: l.ledger,
idx: idx,
addr: addr,
}, true
}
func (l *ledgerKeychain) Addresses() set.Set[ids.ShortID] {
return l.addrs
}
func (l *ledgerSigner) SignHash(hash []byte) ([]byte, error) {
return l.ledger.SignHash(hash, l.idx)
}
func (l *ledgerSigner) Sign(hash []byte) ([]byte, error) {
return l.ledger.Sign(hash, l.idx)
}
func (l *ledgerSigner) Address() ids.ShortID {
return l.addr
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keychain
import (
"testing"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
// mockLedger implements Ledger interface for testing
type mockLedger struct {
addresses map[uint32]ids.ShortID
}
func newMockLedger() *mockLedger {
return &mockLedger{
addresses: make(map[uint32]ids.ShortID),
}
}
func (m *mockLedger) Address(_ string, addressIndex uint32) (ids.ShortID, error) {
if addr, ok := m.addresses[addressIndex]; ok {
return addr, nil
}
// Generate a deterministic address for the index
var addr ids.ShortID
addr[0] = byte(addressIndex)
m.addresses[addressIndex] = addr
return addr, nil
}
func (m *mockLedger) GetAddresses(addressIndices []uint32) ([]ids.ShortID, error) {
addrs := make([]ids.ShortID, len(addressIndices))
for i, idx := range addressIndices {
addr, err := m.Address("", idx)
if err != nil {
return nil, err
}
addrs[i] = addr
}
return addrs, nil
}
func (m *mockLedger) SignHash(_ []byte, _ uint32) ([]byte, error) {
return []byte("mock-hash-signature"), nil
}
func (m *mockLedger) Sign(_ []byte, _ uint32) ([]byte, error) {
return []byte("mock-signature"), nil
}
func (m *mockLedger) SignTransaction(_ []byte, addressIndices []uint32) ([][]byte, error) {
sigs := make([][]byte, len(addressIndices))
for i := range addressIndices {
sigs[i] = []byte("mock-tx-signature")
}
return sigs, nil
}
func (m *mockLedger) Disconnect() error {
return nil
}
func TestNewLedgerKeychain(t *testing.T) {
require := require.New(t)
ledger := newMockLedger()
// Test with empty indices
_, err := NewLedgerKeychain(ledger, []uint32{})
require.ErrorIs(err, ErrInvalidIndicesLength)
// Test with valid indices
kc, err := NewLedgerKeychain(ledger, []uint32{0, 1, 2})
require.NoError(err)
require.NotNil(kc)
// Should have 3 addresses
addrs := kc.Addresses()
require.Equal(3, addrs.Len())
}
func TestLedgerKeychainGetSigner(t *testing.T) {
require := require.New(t)
ledger := newMockLedger()
kc, err := NewLedgerKeychain(ledger, []uint32{0, 1})
require.NoError(err)
// Get address for index 0
addr0, err := ledger.Address("", 0)
require.NoError(err)
// Should be able to get signer for addr0
signer, ok := kc.Get(addr0)
require.True(ok)
require.NotNil(signer)
require.Equal(addr0, signer.Address())
// Should not find unknown address
var unknownAddr ids.ShortID
unknownAddr[0] = 99
_, ok = kc.Get(unknownAddr)
require.False(ok)
}
func TestLedgerSignerSign(t *testing.T) {
require := require.New(t)
ledger := newMockLedger()
kc, err := NewLedgerKeychain(ledger, []uint32{0})
require.NoError(err)
addr0, err := ledger.Address("", 0)
require.NoError(err)
signer, ok := kc.Get(addr0)
require.True(ok)
// Test SignHash
sig, err := signer.SignHash([]byte("test-hash"))
require.NoError(err)
require.Equal([]byte("mock-hash-signature"), sig)
// Test Sign
sig, err = signer.Sign([]byte("test-data"))
require.NoError(err)
require.Equal([]byte("mock-signature"), sig)
}