Skip to content

Commit

Permalink
Merge pull request #45 from nervina-labs/feat/sign-challenge
Browse files Browse the repository at this point in the history
Add calculateChallenge and buildSignedTx functions
  • Loading branch information
yuche authored Dec 16, 2024
2 parents c0cdbef + 51a563d commit 4eaced3
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-ducks-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@joyid/ckb': minor
---

feat: Add calculateChallenge and buildSignedTx functions
112 changes: 111 additions & 1 deletion packages/ckb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,21 @@ import {
} from '@joyid/common'
import * as ckbUtils from '@nervosnetwork/ckb-sdk-utils'
import { Aggregator } from './aggregator'
import { deserializeWitnessArgs } from './utils'

export * from './verify'

const { addressToScript, blake160, serializeScript } = ckbUtils
const {
PERSONAL,
addressToScript,
blake160,
blake2b,
hexToBytes,
toUint64Le,
serializeScript,
rawTransactionToHash,
serializeWitnessArgs,
} = ckbUtils

const appendPrefix = (tokenKey?: string): string | undefined => {
if (!tokenKey) {
Expand Down Expand Up @@ -63,6 +74,7 @@ export const initConfig = (config?: CkbDappConfig): CkbDappConfig => {
export const getConfig = (): CkbDappConfig => internalDappConfig

// The witnessIndexes represents the positions of the JoyID cells in inputs, the default value is an empty array
// e.g. If the transaction inputs have two JoyID cells whose positions are 1 and 3(zero-based index), the witnessIndexes should be [1, 3]
export type SignConfig = CkbDappConfig &
Pick<PopupConfigOptions, 'timeoutInSeconds' | 'popup'> & {
witnessIndexes?: number[]
Expand Down Expand Up @@ -234,6 +246,104 @@ export const signRawTransaction = async (
return res.tx
}

const SECP256R1_PUBKEY_SIG_LEN = 129
// The witnessIndexes represents the positions of the JoyID cells in inputs, the default value is an array containing only 0
// e.g. If the transaction inputs have two JoyID cells whose positions are 1 and 3(zero-based index), the witnessIndexes should be [1, 3]
export const calculateChallenge = async (
tx: CKBTransaction,
witnessIndexes = [0]
): Promise<string> => {
const { witnesses } = tx
if (witnesses.length === 0) {
throw new Error('Witnesses cannot be empty')
}

if (witnessIndexes.length === 0) {
throw new Error('JoyID witnesses can not be empty')
}

const firstWitnessIndex = witnessIndexes[0] ?? 0
if (typeof tx.witnesses[firstWitnessIndex] !== 'string') {
throw new TypeError(
'The first JoyID witness must be serialized hex string of WitnessArgs'
)
}
const transactionHash = rawTransactionToHash(tx)
const witnessArgs = deserializeWitnessArgs(tx.witnesses[firstWitnessIndex]!)

const emptyWitness: CKBComponents.WitnessArgs = {
...witnessArgs,
lock: `0x${'00'.repeat(SECP256R1_PUBKEY_SIG_LEN)}`,
}

console.log(console.log(emptyWitness))

const serializedEmptyWitnessBytes = hexToBytes(
serializeWitnessArgs(emptyWitness)
)
const serializedEmptyWitnessSize = serializedEmptyWitnessBytes.length

const hasher = blake2b(32, null, null, PERSONAL)
hasher.update(hexToBytes(transactionHash))
hasher.update(
hexToBytes(toUint64Le(`0x${serializedEmptyWitnessSize.toString(16)}`))
)
hasher.update(serializedEmptyWitnessBytes)

for (const witnessIndex of witnessIndexes.slice(1)) {
const witness = witnesses[witnessIndex]
if (witness) {
const bytes = hexToBytes(
typeof witness === 'string' ? witness : serializeWitnessArgs(witness)
)
hasher.update(hexToBytes(toUint64Le(`0x${bytes.length.toString(16)}`)))
hasher.update(bytes)
}
}

if (witnesses.length > tx.inputs.length) {
for (const witness of witnesses.slice(tx.inputs.length)) {
const bytes = hexToBytes(
typeof witness === 'string' ? witness : serializeWitnessArgs(witness)
)
hasher.update(hexToBytes(toUint64Le(`0x${bytes.length.toString(16)}`)))
hasher.update(bytes)
}
}

const challenge = `${hasher.digest('hex')}`
return challenge
}

const WITNESS_NATIVE_MODE = '01'
const WITNESS_SUBKEY_MODE = '02'
// The witnessIndexes represents the positions of the JoyID cells in inputs, the default value is an array containing only 0
// e.g. If the transaction inputs have two JoyID cells whose positions are 1 and 3(zero-based index), the witnessIndexes should be [1, 3]
export const buildSignedTx = (
unsignedTx: CKBTransaction,
signedData: SignMessageResponseData,
witnessIndexes = [0]
): CKBTransaction => {
if (unsignedTx.witnesses.length === 0) {
throw new Error('Witnesses length error')
}
const firstWitnessIndex = witnessIndexes[0] ?? 0
const firstWitness = unsignedTx.witnesses[firstWitnessIndex]!
const witnessArgs = deserializeWitnessArgs(firstWitness)

const { message, signature, pubkey, keyType } = signedData

const mode = keyType === 'sub_key' ? WITNESS_SUBKEY_MODE : WITNESS_NATIVE_MODE
witnessArgs.lock = `0x${mode}${pubkey}${signature}${message}`

const signedWitness = append0x(serializeWitnessArgs(witnessArgs))

const signedTx = unsignedTx
signedTx.witnesses[firstWitnessIndex] = signedWitness

return signedTx
}

export const getSubkeyUnlock = async (
aggregatorUrl: string,
connection: AuthResponseData
Expand Down
90 changes: 90 additions & 0 deletions packages/ckb/src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, it, expect } from 'vitest'
import { deserializeWitnessArgs } from './utils'
import { calculateChallenge, CKBTransaction } from '.'

describe('utils', () => {
describe('deserializeWitnessArgs', () => {
it('default_witness_args', async () => {
const witnessArgs = deserializeWitnessArgs(
'0x55000000100000005500000055000000410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
)
expect(witnessArgs.lock).toBe(`0x${'00'.repeat(65)}`)
expect(witnessArgs.inputType).toBe('0x')
expect(witnessArgs.outputType).toBe('0x')
})

it('custom_witness_args', async () => {
const witnessArgs = deserializeWitnessArgs(
'0x1a00000010000000100000001500000001000000100100000020'
)
expect(witnessArgs.lock).toBe('0x')
expect(witnessArgs.inputType).toBe('0x10')
expect(witnessArgs.outputType).toBe('0x20')
})
})

describe('calculateChallenge', () => {
it('verify_challenge_with_raw_tx', async () => {
const ckbTx: CKBComponents.RawTransactionToSign = {
version: '0x00',
cellDeps: [
{
outPoint: {
txHash:
'0x069ae648ecc682caa52b1a6a5854ec3545a8513dd9681f452049a59be33465b0',
index: '0x0',
},
depType: 'depGroup',
},
{
outPoint: {
txHash:
'0xd8c7396f955348bd74a8ed4398d896dad931977b7c1e3f117649765cd3d75b86',
index: '0x0',
},
depType: 'depGroup',
},
],
headerDeps: [],
inputs: [
{
previousOutput: {
txHash:
'0x00ff48f637e12c8aa873d76cd1a9c3e3756f3e7df006760270f23a3a25782f87',
index: '0x1',
},
since: '0x0',
},
],
outputs: [
{
capacity: '0x37e11d0ed',
lock: {
codeHash:
'0xd23761b364210735c19c60561d213fb3beae2fd6172743719eff6920e020baac',
hashType: 'type',
args: '0x0001fd879d61c8187b4fa1e87296bb00371bfebc3a4e',
},
type: {
codeHash:
'0x89cd8003a0eaf8e65e0c31525b7d1d5c1becefd2ea75bb4cff87810ae37764d8',
hashType: 'type',
args: '0x226192f9ca4bcd697bd2fe4429f73a254de25e61',
},
},
],
outputsData: [
'0x020000000000000000000000000000000000000000000000000000000000000000',
],
witnesses: [
'0xc3010000100000001000000010000000af0100007b226964223a2243544d657461222c22766572223a22312e30222c226d65746164617461223a7b22746172676574223a226f75747075742330222c2274797065223a226a6f795f6964222c2264617461223a7b22616c67223a2230783031222c22636f746143656c6c4964223a22307830303030303030303030303030303030222c2263726564656e7469616c4964223a22307837363431643666613336316434326431663830613638396339376430656166376662643336663366303430343332626338313564333932643461366433356162222c2266726f6e745f656e64223a226a6f7969642d6170702d6465762e76657263656c2e617070222c226e616d65223a2273746f6e656b696e67222c227075625f6b6579223a2230786462333135323532356133353836616636346230636638376331636636626633613861663864633961323365383761356161366164316430633665303434376635316631343763363038613831613566383435303936326662383634386465653961636435343863623438626430656438383634326237356530373963616365222c2276657273696f6e223a2230227d7d7d',
],
}

const challenge = await calculateChallenge(ckbTx as CKBTransaction)
expect(challenge).toBe(
'8a4ae0acf8e6481a748627543225f497eaea5d754c9d72cfba8db800ac6d4d1b'
)
})
})
})
29 changes: 29 additions & 0 deletions packages/ckb/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/* eslint-disable unicorn/prefer-string-slice */

import { append0x, remove0x } from '@joyid/common'
import { hexToBytes } from '@nervosnetwork/ckb-sdk-utils'

/**
* Web crypto use IEEE P1363 ECDSA signature format
* ref: https://stackoverflow.com/questions/39554165/ecdsa-signatures-between-node-js-and-webcrypto-appear-to-be-incompatible
Expand All @@ -18,3 +22,28 @@ export function derToIEEE(sig: ArrayBuffer): Uint8Array {
p1363Sig.match(/[\da-f]{2}/gi)!.map((h) => Number.parseInt(h, 16))
)
}

export function leHexStringToU32(hex: string): number {
const bytes = hexToBytes(append0x(hex))
const beHex = `0x${bytes.reduceRight((pre, cur) => pre + cur.toString(16).padStart(2, '0'), '')}`
return Number.parseInt(beHex)
}

export function deserializeWitnessArgs(hex: string): CKBComponents.WitnessArgs {
const args = remove0x(hex)
// full_size(4bytes) + offsets(4bytes * 3) + body(lock + input_type + output_type)
const lockOffset = leHexStringToU32(args.slice(8, 16)) * 2
const inputTypeOffset = leHexStringToU32(args.slice(16, 24)) * 2
const outputTypeOffset = leHexStringToU32(args.slice(24, 32)) * 2

// lock = size(4bytes) + body
const lock = args.slice(lockOffset, inputTypeOffset).slice(8)
const inputType = args.slice(inputTypeOffset, outputTypeOffset).slice(8)
const outputType = args.slice(outputTypeOffset).slice(8)

return {
lock: lock.length === 0 ? '0x' : `0x${lock}`,
inputType: inputType.length === 0 ? '0x' : `0x${inputType}`,
outputType: outputType.length === 0 ? '0x' : `0x${outputType}`,
}
}
14 changes: 5 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4eaced3

Please sign in to comment.