github.com/MetalBlockchain/metalgo@v1.11.9/utils/crypto/ledger/ledger.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package ledger
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/MetalBlockchain/metalgo/ids"
    10  	"github.com/MetalBlockchain/metalgo/utils/crypto/keychain"
    11  	"github.com/MetalBlockchain/metalgo/utils/hashing"
    12  	"github.com/MetalBlockchain/metalgo/version"
    13  
    14  	ledger "github.com/ava-labs/ledger-avalanche/go"
    15  	bip32 "github.com/tyler-smith/go-bip32"
    16  )
    17  
    18  const (
    19  	rootPath          = "m/44'/9000'/0'" // BIP44: m / purpose' / coin_type' / account'
    20  	ledgerBufferLimit = 8192
    21  	ledgerPathSize    = 9
    22  )
    23  
    24  var _ keychain.Ledger = (*Ledger)(nil)
    25  
    26  // Ledger is a wrapper around the low-level Ledger Device interface that
    27  // provides Avalanche-specific access.
    28  type Ledger struct {
    29  	device *ledger.LedgerAvalanche
    30  	epk    *bip32.Key
    31  }
    32  
    33  func New() (keychain.Ledger, error) {
    34  	device, err := ledger.FindLedgerAvalancheApp()
    35  	return &Ledger{
    36  		device: device,
    37  	}, err
    38  }
    39  
    40  func addressPath(index uint32) string {
    41  	return fmt.Sprintf("%s/0/%d", rootPath, index)
    42  }
    43  
    44  func (l *Ledger) Address(hrp string, addressIndex uint32) (ids.ShortID, error) {
    45  	resp, err := l.device.GetPubKey(addressPath(addressIndex), true, hrp, "")
    46  	if err != nil {
    47  		return ids.ShortEmpty, err
    48  	}
    49  	return ids.ToShortID(resp.Hash)
    50  }
    51  
    52  func (l *Ledger) Addresses(addressIndices []uint32) ([]ids.ShortID, error) {
    53  	if l.epk == nil {
    54  		pk, chainCode, err := l.device.GetExtPubKey(rootPath, false, "", "")
    55  		if err != nil {
    56  			return nil, err
    57  		}
    58  		l.epk = &bip32.Key{
    59  			Key:       pk,
    60  			ChainCode: chainCode,
    61  		}
    62  	}
    63  	// derivation path rootPath/0 (BIP44 change level, when set to 0, known as external chain)
    64  	externalChain, err := l.epk.NewChildKey(0)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	addresses := make([]ids.ShortID, len(addressIndices))
    69  	for i, addressIndex := range addressIndices {
    70  		// derivation path rootPath/0/v (BIP44 address index level)
    71  		address, err := externalChain.NewChildKey(addressIndex)
    72  		if err != nil {
    73  			return nil, err
    74  		}
    75  		copy(addresses[i][:], hashing.PubkeyBytesToAddress(address.Key))
    76  	}
    77  	return addresses, nil
    78  }
    79  
    80  func convertToSigningPaths(input []uint32) []string {
    81  	output := make([]string, len(input))
    82  	for i, v := range input {
    83  		output[i] = fmt.Sprintf("0/%d", v)
    84  	}
    85  	return output
    86  }
    87  
    88  func (l *Ledger) SignHash(hash []byte, addressIndices []uint32) ([][]byte, error) {
    89  	strIndices := convertToSigningPaths(addressIndices)
    90  	response, err := l.device.SignHash(rootPath, strIndices, hash)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("%w: unable to sign hash", err)
    93  	}
    94  	responses := make([][]byte, len(addressIndices))
    95  	for i, index := range strIndices {
    96  		sig, ok := response.Signature[index]
    97  		if !ok {
    98  			return nil, fmt.Errorf("missing signature %s", index)
    99  		}
   100  		responses[i] = sig
   101  	}
   102  	return responses, nil
   103  }
   104  
   105  func (l *Ledger) Sign(txBytes []byte, addressIndices []uint32) ([][]byte, error) {
   106  	// will pass to the ledger addressIndices both as signing paths and change paths
   107  	numSigningPaths := len(addressIndices)
   108  	numChangePaths := len(addressIndices)
   109  	if len(txBytes)+(numSigningPaths+numChangePaths)*ledgerPathSize > ledgerBufferLimit {
   110  		// There is a limit on the tx length that can be parsed by the ledger
   111  		// app. When the tx that is being signed is too large, we sign with hash
   112  		// instead.
   113  		//
   114  		// Ref: https://github.com/ava-labs/avalanche-wallet-sdk/blob/9a71f05e424e06b94eaccf21fd32d7983ed1b040/src/Wallet/Ledger/provider/ZondaxProvider.ts#L68
   115  		unsignedHash := hashing.ComputeHash256(txBytes)
   116  		return l.SignHash(unsignedHash, addressIndices)
   117  	}
   118  	strIndices := convertToSigningPaths(addressIndices)
   119  	response, err := l.device.Sign(rootPath, strIndices, txBytes, strIndices)
   120  	if err != nil {
   121  		return nil, fmt.Errorf("%w: unable to sign transaction", err)
   122  	}
   123  	responses := make([][]byte, len(strIndices))
   124  	for i, index := range strIndices {
   125  		sig, ok := response.Signature[index]
   126  		if !ok {
   127  			return nil, fmt.Errorf("missing signature %s", index)
   128  		}
   129  		responses[i] = sig
   130  	}
   131  	return responses, nil
   132  }
   133  
   134  func (l *Ledger) Version() (*version.Semantic, error) {
   135  	resp, err := l.device.GetVersion()
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	return &version.Semantic{
   140  		Major: int(resp.Major),
   141  		Minor: int(resp.Minor),
   142  		Patch: int(resp.Patch),
   143  	}, nil
   144  }
   145  
   146  func (l *Ledger) Disconnect() error {
   147  	return l.device.Close()
   148  }