Skip to content

Commit

Permalink
M2 features (#6)
Browse files Browse the repository at this point in the history
* Feat/metadata (#4)

* Add tx metadata instruction and handle it correctly

* Remove address index from sign method

* not send metadata if it is empty

---------

Co-authored-by: abenso <[email protected]>

* Spend auth signature (#5)

* add spend auth and delegator vote signatures

---------

Co-authored-by: Natanael Mojica <[email protected]>
  • Loading branch information
abenso and neithanmo authored Dec 23, 2024
1 parent eab2942 commit d0af0e4
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 20 deletions.
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[]
}

0 comments on commit d0af0e4

Please sign in to comment.