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

Multi script group witness signing #1055

Merged
merged 5 commits into from
Nov 8, 2019
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
83 changes: 51 additions & 32 deletions packages/neuron-wallet/src/services/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AccountExtendedPublicKey, PathAndPrivateKey } from 'models/keys/key'
import Keystore from 'models/keys/keystore'
import Store from 'models/store'
import LockUtils from 'models/lock-utils'
import { TransactionWithoutHash, Input } from 'types/cell-types'
import { TransactionWithoutHash, Input, WitnessArgs } from 'types/cell-types'
import ConvertTo from 'types/convert-to'
import { WalletNotFound, IsRequired, UsedName } from 'exceptions'
import { Address as AddressInterface } from 'database/address/dao'
Expand Down Expand Up @@ -344,13 +344,8 @@ export default class WalletService {
return this.sendTx(walletID, tx, password, description)
}

public sendTx = async (
walletID: string = '',
tx: TransactionWithoutHash,
password: string = '',
description: string = ''
) => {
const wallet = await this.get(walletID)
public sendTx = async (walletID: string = '', tx: TransactionWithoutHash, password: string = '', description: string = '') => {
const wallet = this.get(walletID)
if (!wallet) {
throw new WalletNotFound(walletID)
}
Expand All @@ -359,32 +354,59 @@ export default class WalletService {
throw new IsRequired('Password')
}

const addressInfos = await this.getAddressInfos(walletID)

let txHash: string = core.utils.rawTransactionToHash(ConvertTo.toSdkTxWithoutHash(tx))
if (!txHash.startsWith('0x')) {
txHash = `0x${txHash}`
}

const { inputs } = tx
const txHash = core.utils.rawTransactionToHash(ConvertTo.toSdkTxWithoutHash(tx))

const addressInfos = await this.getAddressInfos(walletID)
const paths = addressInfos.map(info => info.path)
const pathAndPrivateKeys = this.getPrivateKeys(wallet, paths, password)

const witnesses: string[] = inputs!.map((input: Input) => {
const blake160: string = input.lock!.args!
const info = addressInfos.find(i => i.blake160 === blake160)
const { path } = info!
const findPrivateKey = (blake160: string) => {
const { path } = addressInfos.find(i => i.blake160 === blake160)!
const pathAndPrivateKey = pathAndPrivateKeys.find(p => p.path === path)
if (!pathAndPrivateKey) {
throw new Error('no private key found')
}
const { privateKey } = pathAndPrivateKey
const witness = this.signWitness('', privateKey, txHash)
return witness
return pathAndPrivateKey.privateKey
}

const witnessSigningEntries = tx.inputs!.map((input: Input) => {
const blake160: string = input.lock!.args!
const witnessArgs: WitnessArgs = {
lock: undefined,
inputType: undefined,
outputType: undefined
}
return {
// TODO: fill in required DAO's type witness here
witnessArgs,
lockHash: input.lockHash!,
witness: '',
blake160,
}
})

tx.witnesses = witnesses
const lockHashes = new Set(witnessSigningEntries.map(w => w.lockHash))

for (const lockHash of lockHashes) {
const firstIndex = witnessSigningEntries.findIndex(w => w.lockHash === lockHash)
const witnessesArgs = witnessSigningEntries.filter(w => w.lockHash === lockHash)
// A 65-byte empty signature used as placeholder
witnessesArgs[0].witnessArgs.lock = '0x' + '0'.repeat(130)

const privateKey = findPrivateKey(witnessesArgs[0].blake160)
const signedWitness = core.signWitnesses(privateKey)({
transactionHash: txHash,
witnesses: witnessesArgs.map(w => w.witnessArgs)
})[0] as string

for (const w of witnessSigningEntries) {
if (w.lockHash === lockHash) {
w.witness = '0x'
}
}
witnessSigningEntries[firstIndex].witness = signedWitness
}

tx.witnesses = witnessSigningEntries.map(w => w.witness)

const txToSend = ConvertTo.toSdkTxWithoutHash(tx)
await core.rpc.sendTransaction(txToSend)
Expand All @@ -403,9 +425,7 @@ export default class WalletService {
return txHash
}

public calculateFee = async (
tx: TransactionWithoutHash
) => {
public calculateFee = async (tx: TransactionWithoutHash) => {
const inputCapacities = tx.inputs!
.map(input => BigInt(input.capacity!))
.reduce((result, c) => result + c, BigInt(0))
Expand Down Expand Up @@ -474,12 +494,11 @@ export default class WalletService {

// path is a BIP44 full path such as "m/44'/309'/0'/0/0"
public getAddressInfos = async (walletID: string): Promise<AddressInterface[]> => {
const wallet = await this.get(walletID)
const wallet = this.get(walletID)
if (!wallet) {
throw new WalletNotFound(walletID)
}
const addrs = await AddressService.allAddressesByWalletId(walletID)
return addrs
return AddressService.allAddressesByWalletId(walletID)
}

public getChangeAddress = async (): Promise<string> => {
Expand All @@ -500,7 +519,7 @@ export default class WalletService {
})[0] as string
}

// Derivate all child private keys for specified BIP44 paths.
// Derive all child private keys for specified BIP44 paths.
public getPrivateKeys = (wallet: Wallet, paths: string[], password: string): PathAndPrivateKey[] => {
const masterPrivateKey = wallet.loadKeystore().extendedPrivateKey(password)
const masterKeychain = new Keychain(
Expand Down
6 changes: 6 additions & 0 deletions packages/neuron-wallet/src/types/cell-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export interface Transaction extends TransactionWithoutHash {
hash: string
}

export interface WitnessArgs {
lock: string | undefined
inputType: string | undefined
outputType: string | undefined
}

export interface TxStatus {
blockHash: string | null
status: 'pending' | 'proposed' | 'committed'
Expand Down