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

M2 features #6

Merged
merged 2 commits into from
Dec 23, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Main"
name: 'Main'
on:
- push

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Publish packages"
name: 'Publish packages'

on:
release:
Expand All @@ -25,8 +25,8 @@ jobs:
- name: Install node
uses: actions/setup-node@v4
with:
registry-url: "https://registry.npmjs.org"
scope: "@zondax"
registry-url: 'https://registry.npmjs.org'
scope: '@zondax'
- run: mv README-npm.md README.md
- name: Install yarn
run: npm install -g yarn
Expand Down
132 changes: 123 additions & 9 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@
import BaseApp, {
BIP32Path,
ConstructorParams,
LedgerError,
PAYLOAD_TYPE,
Transport,
processErrorResponse,
processResponse,
} from '@zondax/ledger-js'

import { DEFAULT_PATH, P2_VALUES, PREHASH_LEN, RANDOMIZER_LEN, SIGRSLEN } from './consts'
import { processGetAddrResponse, processGetFvkResponse } from './helper'
import { AddressIndex, PenumbraIns, ResponseAddress, ResponseFvk, ResponseSign } from './types'
import { DEFAULT_PATH, P2_VALUES, RANDOMIZER_LEN, SPEND_AUTH_SIGNATURE_LEN, DELEGATOR_VOTE_SIGNATURE_LEN } from './consts'
import { processGetAddrResponse, processGetFvkResponse, processEffectHashResponse } from './helper'
import { AddressIndex, PenumbraIns, ResponseAddress, ResponseFvk, ResponseSign, ResponseEffectHash } from './types'
import { ByteStream } from '@zondax/ledger-js/dist/byteStream'

// https://buf.build/penumbra-zone/penumbra/docs/main:penumbra.custody.v1#penumbra.custody.v1.ConfirmAddressRequest

Expand All @@ -44,6 +43,9 @@ export class PenumbraApp extends BaseApp {
GET_ADDR: 0x01,
SIGN: 0x02,
FVK: 0x03,
TX_METADATA: 0x04,
GET_SPEND_AUTH_SIGNATURES: 0x05,
GET_DELEGATOR_VOTE_SIGNATURES: 0x06,
},
p1Values: {
ONLY_RETRIEVE: 0x00,
Expand Down Expand Up @@ -111,22 +113,134 @@ export class PenumbraApp extends BaseApp {
}
}

async sign(path: BIP32Path, addressIndex: AddressIndex, blob: Buffer): Promise<ResponseSign> {
async sign(path: BIP32Path, blob: Buffer, metadata: string[] = []): Promise<ResponseSign> {
const chunks = this.prepareChunks(path, blob)
try {
// First send the metadata
if (metadata.length !== 0) {
await this._sendTxMetadata(metadata)
}

let signatureResponse = await this.signSendChunk(this.INS.SIGN, 1, chunks.length, chunks[0])

for (let i = 1; i < chunks.length; i += 1) {
signatureResponse = await this.signSendChunk(this.INS.SIGN, 1 + i, chunks.length, chunks[i])
}

// | 64 bytes | 2 bytes | 2 bytes |
// | effect hash | spend auth signature qty | delegator vote signature qty |
let effectHashSignatures = processEffectHashResponse(signatureResponse)

// Get spend auth signatures
let spendAuthSignatures = []
for (let i = 0; i < effectHashSignatures.spendAuthSignatureQty; i++) {
spendAuthSignatures.push(await this._getSpendAuthSignatures(i))
}

// Get delegator vote signatures
let delegatorVoteSignatures = []
for (let i = 0; i < effectHashSignatures.delegatorVoteSignatureQty; i++) {
delegatorVoteSignatures.push(await this._getDelegatorVoteSignatures(i))
}

return {
signature: signatureResponse.readBytes(signatureResponse.length()),
effectHash: effectHashSignatures.effectHash,
spendAuthSignatures,
delegatorVoteSignatures,
}

} catch (e) {
throw processErrorResponse(e)
}
}

private async _getSpendAuthSignatures(index: number): Promise<Buffer> {
try {
if (index > 255) {
throw new Error('Index for obtaining spend authorization signatures cannot exceed 255')
}

const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_SPEND_AUTH_SIGNATURES, index, P2_VALUES.DEFAULT)

const payload = processResponse(responseBuffer)
const spendAuthSignature = payload.readBytes(SPEND_AUTH_SIGNATURE_LEN)

return spendAuthSignature
} catch (e) {
throw processErrorResponse(e)
}
}

private async _getDelegatorVoteSignatures(index: number): Promise<Buffer> {
try {
if (index > 255) {
throw new Error('Index for obtaining delegator vote signatures cannot exceed 255')
}
const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_DELEGATOR_VOTE_SIGNATURES, index, P2_VALUES.DEFAULT)
const payload = processResponse(responseBuffer)
const delegatorVoteSignature = payload.readBytes(DELEGATOR_VOTE_SIGNATURE_LEN)
return delegatorVoteSignature
} catch (e) {
throw processErrorResponse(e)
}
}

/**
* Converts an array of strings into a single Buffer with the format:
* length + string bytes + length + string bytes + ...
*
* @param metadata - An array of strings to be converted.
* @returns A Buffer containing the length-prefixed string bytes.
* @throws Will throw an error if any string exceeds 120 bytes when encoded.
*/
private _convertMetadataToBuffer(metadata: string[]): Buffer {
const buffers: Buffer[] = []

// Prepend the number of strings as UInt8
const numStrings = metadata.length
if (numStrings > 255) {
throw new Error('Cannot have more than 255 metadata strings')
}
const numStringsBuffer = Buffer.from([numStrings])
buffers.push(numStringsBuffer)

for (const data of metadata) {
// Encode the string into a Buffer using UTF-8 encoding
const dataBuffer = Buffer.from(data, 'utf8')
const length = dataBuffer.length

// Validate the length
if (length > 120) {
throw new Error('Each metadata string must be 120 bytes or fewer.')
}

// Create a Buffer for the length (UInt8 since max length is 120)
const lengthBuffer = Buffer.from([length])

// Append the length and data buffers to the array
buffers.push(lengthBuffer, dataBuffer)
}

// Concatenate all buffers into one
return Buffer.concat(buffers)
}

private async _sendTxMetadata(metadata: string[]): Promise<void> {
const metadataBuffer = this._convertMetadataToBuffer(metadata)
const chunks = this.messageToChunks(metadataBuffer)

try {
for (let i = 0; i < chunks.length; i++) {
const chunkIdx = i + 1
const chunkNum = chunks.length

await this.sendGenericChunk(this.INS.TX_METADATA, 0, chunkIdx, chunkNum, chunks[i])
}
} catch (error) {
throw processErrorResponse(error)
}
}

private _prepareAddressData(path: string, addressIndex: AddressIndex): Buffer {
// Path must always be this
// according to penumbra team
Expand All @@ -136,15 +250,15 @@ export class PenumbraApp extends BaseApp {
// Enforce exactly 3 levels
// this was set in our class constructor [3]
const serializedPath = this.serializePath(path)
const accountBuffer = this.serializeAccountIndex(addressIndex)
const accountBuffer = this._serializeAccountIndex(addressIndex)

// concatenate data
const concatenatedBuffer: Buffer = Buffer.concat([serializedPath, accountBuffer])

return concatenatedBuffer
}

private serializeAccountIndex(accountIndex: AddressIndex): Buffer {
private _serializeAccountIndex(accountIndex: AddressIndex): Buffer {
const accountBuffer = Buffer.alloc(4)
accountBuffer.writeUInt32LE(accountIndex.account)

Expand Down
3 changes: 3 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ export const FVKLEN = AK_LEN + NK_LEN
export const SIGRSLEN = 64
export const PREHASH_LEN = 32
export const RANDOMIZER_LEN = 12
export const EFFECT_HASH_LEN = 64
export const SPEND_AUTH_SIGNATURE_LEN = 64
export const DELEGATOR_VOTE_SIGNATURE_LEN = 64
16 changes: 14 additions & 2 deletions src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ResponsePayload } from '@zondax/ledger-js/dist/payload'

import { ADDRLEN, AK_LEN, FVKLEN, NK_LEN } from './consts'
import { ResponseAddress, ResponseFvk } from './types'
import { ADDRLEN, AK_LEN, EFFECT_HASH_LEN, FVKLEN } from './consts'
import { ResponseAddress, ResponseEffectHash, ResponseFvk } from './types'

export function processGetAddrResponse(response: ResponsePayload): ResponseAddress {
const address = response.readBytes(ADDRLEN)
Expand All @@ -23,3 +23,15 @@ export function processGetFvkResponse(response: ResponsePayload): ResponseFvk {
nk,
}
}

export function processEffectHashResponse(response: ResponsePayload): ResponseEffectHash {
const effectHash = response.readBytes(EFFECT_HASH_LEN)
const spendAuthSignatureQty = response.readBytes(2).readUInt16LE(0)
const delegatorVoteSignatureQty = response.readBytes(2).readUInt16LE(0)

return {
effectHash,
spendAuthSignatureQty,
delegatorVoteSignatureQty,
}
}
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from "./app";
export * from "./types";
export * from "./consts";
export * from "./helper";
export * from './app'
export * from './types'
export * from './consts'
export * from './helper'
15 changes: 14 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export interface PenumbraIns extends INSGeneric {
GET_ADDR: 0x01
SIGN: 0x02
FVK: 0x03
TX_METADATA: 0x04
GET_SPEND_AUTH_SIGNATURES: 0x05
GET_DELEGATOR_VOTE_SIGNATURES: 0x06
}

export interface AddressIndex {
Expand All @@ -26,6 +29,16 @@ export interface ResponseFvk {
nk: Buffer
}

// Effect hash response: First response for the sign procedure
export interface ResponseEffectHash {
effectHash: Buffer
spendAuthSignatureQty: number
delegatorVoteSignatureQty: number
}

// API signature response
export interface ResponseSign {
signature: Buffer
effectHash: Buffer
spendAuthSignatures: Buffer[]
delegatorVoteSignatures: Buffer[]
}
Loading