Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(packages/jellyfish-wallet-ledger): jellyfish-wallet-ledger implementation #493

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ jobs:
node-version: '15'
cache: 'npm'

- run: sudo apt-get update
- run: sudo apt-get install libudev-dev

- run: npm ci
- run: npm run build

Expand Down
158 changes: 158 additions & 0 deletions packages/jellyfish-wallet-ledger/__tests__/bip32.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { LedgerHdNodeProvider } from '../src/hd_node'
import Transport from '@ledgerhq/hw-transport'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import SpeculosTransport from '@ledgerhq/hw-transport-node-speculos'
import { dSHA256, HASH160 } from '@defichain/jellyfish-crypto'
import { OP_CODES, Transaction, Vout } from '@defichain/jellyfish-transaction'
import BigNumber from 'bignumber.js'

const transaction: Transaction = {
version: 0x00000004,
lockTime: 0x00000000,
vin: [{
index: 0,
script: { stack: [] },
sequence: 4294967278,
txid: '9f96ade4b41d5433f4eda31e1738ec2b36f6e7d1420d94a6af99801a88f7f7ff'
}],
vout: [{
script: {
stack: [
OP_CODES.OP_0,
OP_CODES.OP_PUSHDATA(Buffer.from('1d0f172a0ecb48aee1be1f2687d2963ae33f71a1', 'hex'), 'little')
]
},
value: new BigNumber('5.98'),
tokenId: 0x00
}]
}

const prevout: Vout = {
script: {
stack: [
OP_CODES.OP_0,
OP_CODES.OP_PUSHDATA(Buffer.from('1d0f172a0ecb48aee1be1f2687d2963ae33f71a1', 'hex'), 'little')
]
},
value: new BigNumber('6'),
tokenId: 0x00
}

describe('hardware device tests', function () {
let provider: LedgerHdNodeProvider
let transport: Transport

beforeAll(async () => {
transport = await TransportNodeHid.create()
provider = LedgerHdNodeProvider.getProvider(transport)
})

afterAll(async () => {
await transport.close()
})

it('should get publlic key', async () => {
const ledgerNode = provider.derive("m/44'/1129'/0'/0/0")

const pubKey = await ledgerNode.publicKey(true)
expect(pubKey.length).toStrictEqual(33)
})

it('should sign and verify signature', async () => {
const ledgerNode = provider.derive("m/44'/1129'/0'/0/0")
// sign
const message = Buffer.from('test1', 'utf-8')
const derSignature = await ledgerNode.sign(message)

// construct the data signed by the ledger device
const msgLengthString = message.length < 16 ? '0' + message.length.toString(16) : message.length.toString(16)
const msgLength = Buffer.from(msgLengthString, 'hex')
const inputData = Buffer.concat([Buffer.from('15', 'hex'), Buffer.from('Defi Signed Message:\n', 'utf-8'), msgLength, message])
// calculate the input hash
const hash = dSHA256(inputData)

// verify
const valid = await ledgerNode.verify(hash, derSignature)
expect(valid).toStrictEqual(true)
})

it('should sign tx', async () => {
const ledgerNode = provider.derive("m/44'/1129'/0'/0/0")
const signed = await ledgerNode.signTx(transaction, [{
...prevout,
script: {
stack: [
OP_CODES.OP_0,
OP_CODES.OP_PUSHDATA(HASH160(await ledgerNode.publicKey()), 'little')
]
}
}])

expect(signed.witness.length).toStrictEqual(1)
expect(signed.witness[0].scripts.length).toStrictEqual(2)

expect(signed.witness[0].scripts[0].hex.length).toBeGreaterThanOrEqual(140)
expect(signed.witness[0].scripts[0].hex.length).toBeLessThanOrEqual(144) // NOTE(surangap): High s
expect(signed.witness[0].scripts[1].hex.length).toStrictEqual(66)
})
})

describe('speculos tests', function () {
let provider: LedgerHdNodeProvider
const apduPort = 9999
let transport: Transport

beforeAll(async () => {
transport = await SpeculosTransport.open({ apduPort })
provider = LedgerHdNodeProvider.getProvider(transport)
})

afterAll(async () => {
await transport.close()
})

it('should get publlic key', async () => {
const ledgerNode = provider.derive("m/44'/1129'/0'/0/0")

const pubKey = await ledgerNode.publicKey(true)
expect(pubKey.length).toStrictEqual(33)
})

it('should sign and verify signature', async () => {
const ledgerNode = provider.derive("m/44'/1129'/0'/0/0")
// sign
const message = Buffer.from('test1', 'utf-8')
const derSignature = await ledgerNode.sign(message)

// construct the data signed by the ledger device
const msgLengthString = message.length < 16 ? '0' + message.length.toString(16) : message.length.toString(16)
const msgLength = Buffer.from(msgLengthString, 'hex')
const inputData = Buffer.concat([Buffer.from('15', 'hex'), Buffer.from('Defi Signed Message:\n', 'utf-8'), msgLength, message])
// calculate the input hash
const hash = dSHA256(inputData)

// verify
const valid = await ledgerNode.verify(hash, derSignature)
expect(valid).toStrictEqual(true)
})

it('should sign tx', async () => {
const ledgerNode = provider.derive("m/44'/1129'/0'/0/0")
const signed = await ledgerNode.signTx(transaction, [{
...prevout,
script: {
stack: [
OP_CODES.OP_0,
OP_CODES.OP_PUSHDATA(HASH160(await ledgerNode.publicKey()), 'little')
]
}
}])

expect(signed.witness.length).toStrictEqual(1)
expect(signed.witness[0].scripts.length).toStrictEqual(2)

expect(signed.witness[0].scripts[0].hex.length).toBeGreaterThanOrEqual(140)
expect(signed.witness[0].scripts[0].hex.length).toBeLessThanOrEqual(144) // NOTE(surangap): High s
expect(signed.witness[0].scripts[1].hex.length).toStrictEqual(66)
})
})
12 changes: 12 additions & 0 deletions packages/jellyfish-wallet-ledger/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
testEnvironment: 'node',
testMatch: [
'**/__tests__/**/*.test.ts'
],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true,
clearMocks: true,
testTimeout: 120000
}
55 changes: 55 additions & 0 deletions packages/jellyfish-wallet-ledger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"private": false,
"name": "@defichain/jellyfish-wallet-ledger",
"version": "0.0.0",
"description": "A collection of TypeScript + JavaScript tools and libraries for DeFiChain developers to build decentralized finance on Bitcoin",
"keywords": [
"DeFiChain",
"DeFi",
"Blockchain",
"API",
"Bitcoin"
],
"repository": "DeFiCh/jellyfish",
"bugs": "https://github.com/DeFiCh/jellyfish/issues",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"contributors": [
{
"name": "DeFiChain Foundation",
"email": "engineering@defichain.com",
"url": "https://defichain.com/"
},
{
"name": "DeFi Blockchain Contributors"
},
{
"name": "DeFiChain Jellyfish Contributors"
}
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc -b ./tsconfig.build.json"
},
"dependencies": {
"@defichain/jellyfish-wallet": "0.0.0",
"@defichain/jellyfish-transaction": "0.0.0",
"@ledgerhq/hw-transport": "^6.2.0",
"@ledgerhq/hw-transport-node-hid": "^6.2.0",
"@ledgerhq/hw-app-btc": "^6.2.0",
"tiny-secp256k1": "^1.1.6"
},
"devDependencies": {
"@types/ledgerhq__hw-transport": "^4.21.3",
"@ledgerhq/hw-transport-node-speculos": "^6.2.0",
"@types/ledgerhq__hw-transport-node-hid": "^4.22.2",
"@types/tiny-secp256k1": "^1.0.0",
"@types/ledgerhq__hw-app-btc": "^5.19.3"
}
}
114 changes: 114 additions & 0 deletions packages/jellyfish-wallet-ledger/src/hd_node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { SIGHASH, Transaction, TransactionSegWit, Vout } from '@defichain/jellyfish-transaction'
import { WalletHdNode, WalletHdNodeProvider } from '@defichain/jellyfish-wallet'
import Transport from '@ledgerhq/hw-transport'
import AppBtc from '@ledgerhq/hw-app-btc'
import { DERSignature } from '@defichain/jellyfish-crypto'
import ecc from 'tiny-secp256k1'
import { TransactionSigner } from '@defichain/jellyfish-transaction-signature'

/**
* LedgerHdNode implements the WalletHdNode for ledger hardware device for jellyfish-wallet; a CoinType-agnostic HD Wallet for noncustodial DeFi.
* Purpose [44'] / CoinType-agnostic [n] / Account [n] / Chain (ignored for now) [0] / Addresses [n]
*
* - BIP32 Hierarchical Deterministic Wallets
* - BIP44 Multi-Account Hierarchy for Deterministic Wallets
*/
export class LedgerHdNode implements WalletHdNode {
private readonly transport: Transport
private readonly path: string
private readonly btcApp: AppBtc
private pubKeyCompressed: Buffer | undefined // cache the public key so that we don't have to request for user auth multiple times

constructor (transport: Transport, path: string) {
this.transport = transport
this.path = path
this.btcApp = new AppBtc(this.transport)
}

/**
* Returns the public key.
*
* @return {Promise<Buffer>} Object including the public key in compressed format
*/
async publicKey (verify: boolean = false): Promise<Buffer> {
if (this.pubKeyCompressed === undefined) {
const result = await this.btcApp.getWalletPublicKey(this.path, { verify: verify, format: 'legacy' })
this.pubKeyCompressed = ecc.pointCompress(Buffer.from(result.publicKey, 'hex'), true)
return this.pubKeyCompressed
} else {
return this.pubKeyCompressed
}
}

// NOTE(surangap): private key do not leave the hardware device
async privateKey (): Promise<Buffer> {
throw new Error('private key do not leave the ledger')
}

/**
* Signs the message with the private key.
* The data to sign will be the "\x15Defi Signed Message:\n"<length of the message in 1 byte><message>
*
* @param {Buffer} message message to sign
* @return {Promise<Buffer>} Object including the signature in DER format
*/
async sign (message: Buffer): Promise<Buffer> {
const result = await this.btcApp.signMessageNew(this.path, message.toString('hex'))
const signature = Buffer.concat([Buffer.from(result.r, 'hex'), Buffer.from(result.s, 'hex')])
return DERSignature.encode(signature)
}

/**
* Verifies the given signature with the hash.
*
* @param {Buffer} hash hash of the signed message
* @param {Buffer} derSignature signature in DER format
* @return {Promise<boolean>} Object including the verification result
*/
async verify (hash: Buffer, derSignature: Buffer): Promise<boolean> {
const signature = DERSignature.decode(derSignature)
return ecc.verify(hash, await this.publicKey(), signature)
}

/**
* Sign a transaction with all prevout belong to this HdNode with SIGHASH.ALL
* This implementation can only sign a P2WPKH, hence the implementing WalletAccount should only
* recognize P2WPKH addresses encoded in bech32 format.
*
* @param {Transaction} transaction to sign
* @param {Vout[]} prevouts of transaction to sign, ellipticPair will be mapped to current node
* @return TransactionSegWit signed transaction ready to broadcast
*/
async signTx (transaction: Transaction, prevouts: Vout[]): Promise<TransactionSegWit> {
return await TransactionSigner.signPrevoutsWithEllipticPairs(transaction, prevouts, prevouts.map(() => this), {
sigHashType: SIGHASH.ALL
})
}
}

/**
* Provider that derive LedgerHdNode from the path.
*/
export class LedgerHdNodeProvider implements WalletHdNodeProvider<LedgerHdNode> {
private readonly transport: Transport

private constructor (transport: Transport) {
this.transport = transport
}

/**
* @param {Transport} transport Transport to connect to the ledger hardware device
* @return {LedgerHdNodeProvider} LedgerHdNodeProvider
*/
static getProvider (transport: Transport): LedgerHdNodeProvider {
return new LedgerHdNodeProvider(transport)
}

/**
* @param {string} path bip32 path
* @return {LedgerHdNode} LedgerHdNode for the relavant path
*/
derive (path: string): LedgerHdNode {
return new LedgerHdNode(this.transport, path)
}
}
1 change: 1 addition & 0 deletions packages/jellyfish-wallet-ledger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './hd_node'
10 changes: 10 additions & 0 deletions packages/jellyfish-wallet-ledger/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.build.json",
"include": [
"./src/**/*"
],
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
}
}