mirror of
https://github.com/luxfi/keychain.git
synced 2026-09-24 00:24:11 +00:00
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:
@@ -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
|
||||
)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user