Skip to content

Commit

Permalink
API: Support attaching signatures to standard and multisig transactio…
Browse files Browse the repository at this point in the history
…ns (#595)

* Add attach signature method to transaction class

* Add multisig external signature methods

* Fix failing multisig test

* Add signature length checks

* Add method to create an unsigned multisig transaction blob

* Rename multisig create methods and use unencoded transaction

* Refactor `createMultisigTransactionWithSignature` to use `createMultisigTransaction` method

* Fix algosdk createMultisigTransaction export

* Use MultisigMetadata without pks in new create method

* These types should be consolidated in the future, and addrs seems like a better convention to use long-term

* More descriptive test suite name
  • Loading branch information
jdtzmn authored Jul 28, 2022
1 parent 784baa2 commit 9eaf7f6
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 27 deletions.
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ export {
signMultisigTransaction,
mergeMultisigTransactions,
appendSignMultisigTransaction,
createMultisigTransaction,
appendSignRawMultisigSignature,
verifyMultisig,
multisigAddress,
} from './multisig';
export { SourceMap } from './logic/sourcemap';
Expand Down
154 changes: 127 additions & 27 deletions src/multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const MULTISIG_NO_MUTATE_ERROR_MSG =
'Cannot mutate a multisig field as it would invalidate all existing signatures.';
export const MULTISIG_USE_PARTIAL_SIGN_ERROR_MSG =
'Cannot sign a multisig transaction using `signTxn`. Use `partialSignTxn` instead.';
export const MULTISIG_SIGNATURE_LENGTH_ERROR_MSG =
'Cannot add multisig signature. Signature is not of the correct length.';

interface MultisigOptions {
rawSig: Uint8Array;
Expand All @@ -40,41 +42,27 @@ interface MultisigMetadataWithPks extends Omit<MultisigMetadata, 'addrs'> {
}

/**
* createMultisigTransaction creates a multisig transaction blob.
* @param txnForEncoding - the actual transaction to sign.
* @param rawSig - a Buffer raw signature of that transaction
* @param myPk - a public key that corresponds with rawSig
* createRawMultisigTransaction creates a raw, unsigned multisig transaction blob.
* @param txn - the actual transaction.
* @param version - multisig version
* @param threshold - mutlisig threshold
* @param threshold - multisig threshold
* @param pks - ordered list of public keys in this multisig
* @returns encoded multisig blob
*/
function createMultisigTransaction(
txnForEncoding: EncodedTransaction,
{ rawSig, myPk }: MultisigOptions,
{ version, threshold, pks }: MultisigMetadataWithPks
export function createMultisigTransaction(
txn: txnBuilder.Transaction,
{ version, threshold, addrs }: MultisigMetadata
) {
let keyExist = false;
// construct the appendable multisigned transaction format
const subsigs = pks.map((pk) => {
if (nacl.bytesEqual(pk, myPk)) {
keyExist = true;
return {
pk: Buffer.from(pk),
s: rawSig,
};
}
return { pk: Buffer.from(pk) };
});
if (keyExist === false) {
throw new Error(MULTISIG_KEY_NOT_EXIST_ERROR_MSG);
}
const pks = addrs.map((addr) => address.decodeAddress(addr).publicKey);
const subsigs = pks.map((pk) => ({ pk: Buffer.from(pk) }));

const msig: EncodedMultisig = {
v: version,
thr: threshold,
subsig: subsigs,
};
const txnForEncoding = txn.get_obj_for_encoding();
const signedTxn: EncodedSignedTransaction = {
msig,
txn: txnForEncoding,
Expand All @@ -97,6 +85,58 @@ function createMultisigTransaction(
return new Uint8Array(encoding.encode(signedTxn));
}

/**
* createMultisigTransactionWithSignature creates a multisig transaction blob with an included signature.
* @param txn - the actual transaction to sign.
* @param rawSig - a Buffer raw signature of that transaction
* @param myPk - a public key that corresponds with rawSig
* @param version - multisig version
* @param threshold - multisig threshold
* @param pks - ordered list of public keys in this multisig
* @returns encoded multisig blob
*/
function createMultisigTransactionWithSignature(
txn: txnBuilder.Transaction,
{ rawSig, myPk }: MultisigOptions,
{ version, threshold, pks }: MultisigMetadataWithPks
) {
// Create an empty encoded multisig transaction
const encodedMsig = createMultisigTransaction(txn, {
version,
threshold,
addrs: pks.map((pk) => address.encodeAddress(pk)),
});
// note: this is not signed yet, but will be shortly
const signedTxn = encoding.decode(encodedMsig) as EncodedSignedTransaction;

let keyExist = false;
// append the multisig signature to the corresponding public key in the multisig blob
signedTxn.msig.subsig.forEach((subsig, i) => {
if (nacl.bytesEqual(subsig.pk, myPk)) {
keyExist = true;
signedTxn.msig.subsig[i].s = rawSig;
}
});
if (keyExist === false) {
throw new Error(MULTISIG_KEY_NOT_EXIST_ERROR_MSG);
}

// if the address of this multisig is different from the transaction sender,
// we need to add the auth-addr field
const msigAddr = address.fromMultisigPreImg({
version,
threshold,
pks,
});
if (
address.encodeAddress(signedTxn.txn.snd) !== address.encodeAddress(msigAddr)
) {
signedTxn.sgnr = Buffer.from(msigAddr);
}

return new Uint8Array(encoding.encode(signedTxn));
}

/**
* MultisigTransaction is a Transaction that also supports creating partially-signed multisig transactions.
*/
Expand Down Expand Up @@ -140,13 +180,39 @@ export class MultisigTransaction extends txnBuilder.Transaction {
) {
// get signature verifier
const myPk = nacl.keyPairFromSecretKey(sk).publicKey;
return createMultisigTransaction(
this.get_obj_for_encoding(),
return createMultisigTransactionWithSignature(
this,
{ rawSig: this.rawSignTxn(sk), myPk },
{ version, threshold, pks }
);
}

/**
* partialSignWithMultisigSignature partially signs this transaction with an external raw multisig signature and returns
* a partially-signed multisig transaction, encoded with msgpack as a typed array.
* @param metadata - multisig metadata
* @param signerAddr - address of the signer
* @param signature - raw multisig signature
* @returns an encoded, partially signed multisig transaction.
*/
partialSignWithMultisigSignature(
metadata: MultisigMetadataWithPks,
signerAddr: string,
signature: Uint8Array
) {
if (!nacl.isValidSignatureLength(signature.length)) {
throw new Error(MULTISIG_SIGNATURE_LENGTH_ERROR_MSG);
}
return createMultisigTransactionWithSignature(
this,
{
rawSig: signature,
myPk: address.decodeAddress(signerAddr).publicKey,
},
metadata
);
}

// eslint-disable-next-line camelcase
static from_obj_for_encoding(
txnForEnc: EncodedTransaction
Expand Down Expand Up @@ -312,7 +378,7 @@ export function verifyMultisig(
/**
* signMultisigTransaction takes a raw transaction (see signTransaction), a multisig preimage, a secret key, and returns
* a multisig transaction, which is a blob representing a transaction and multisignature account preimage. The returned
* multisig txn can accumulate additional signatures through mergeMultisigTransactions or appendMultisigTransaction.
* multisig txn can accumulate additional signatures through mergeMultisigTransactions or appendSignMultisigTransaction.
* @param txn - object with either payment or key registration fields
* @param version - multisig version
* @param threshold - multisig threshold
Expand Down Expand Up @@ -391,9 +457,43 @@ export function appendSignMultisigTransaction(
};
}

/**
* appendMultisigTransactionSignature takes a multisig transaction blob, and appends a given raw signature to it.
* This makes it possible to compile a multisig signature using only raw signatures from external methods.
* @param multisigTxnBlob - an encoded multisig txn. Supports non-payment txn types.
* @param version - multisig version
* @param threshold - multisig threshold
* @param addrs - a list of Algorand addresses representing possible signers for this multisig. Order is important.
* @param signerAddr - address of the signer
* @param signature - raw multisig signature
* @returns object containing txID, and blob representing encoded multisig txn
*/
export function appendSignRawMultisigSignature(
multisigTxnBlob: Uint8Array,
{ version, threshold, addrs }: MultisigMetadata,
signerAddr: string,
signature: Uint8Array
) {
const pks = addrs.map((addr) => address.decodeAddress(addr).publicKey);
// obtain underlying txn, sign it, and merge it
const multisigTxObj = encoding.decode(
multisigTxnBlob
) as EncodedSignedTransaction;
const msigTxn = MultisigTransaction.from_obj_for_encoding(multisigTxObj.txn);
const partialSignedBlob = msigTxn.partialSignWithMultisigSignature(
{ version, threshold, pks },
signerAddr,
signature
);
return {
txID: msigTxn.txID().toString(),
blob: mergeMultisigTransactions([multisigTxnBlob, partialSignedBlob]),
};
}

/**
* multisigAddress takes multisig metadata (preimage) and returns the corresponding human readable Algorand address.
* @param version - mutlisig version
* @param version - multisig version
* @param threshold - multisig threshold
* @param addrs - list of Algorand addresses
*/
Expand Down
4 changes: 4 additions & 0 deletions src/nacl/naclWrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export function keyPair() {
return keyPairFromSeed(seed);
}

export function isValidSignatureLength(len: number) {
return len === nacl.sign.signatureLength;
}

export function keyPairFromSecretKey(sk: Uint8Array) {
return nacl.sign.keyPair.fromSecretKey(sk);
}
Expand Down
16 changes: 16 additions & 0 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,22 @@ export class Transaction implements TransactionStorageStructure {
return new Uint8Array(encoding.encode(sTxn));
}

attachSignature(signerAddr: string, signature: Uint8Array) {
if (!nacl.isValidSignatureLength(signature.length)) {
throw new Error('Invalid signature length');
}
const sTxn: EncodedSignedTransaction = {
sig: Buffer.from(signature),
txn: this.get_obj_for_encoding(),
};
// add AuthAddr if signing with a different key than From indicates
if (signerAddr !== address.encodeAddress(this.from.publicKey)) {
const signerPublicKey = address.decodeAddress(signerAddr).publicKey;
sTxn.sgnr = Buffer.from(signerPublicKey);
}
return new Uint8Array(encoding.encode(sTxn));
}

rawTxID() {
const enMsg = this.toByte();
const gh = Buffer.from(utils.concatArrays(this.tag, enMsg));
Expand Down
8 changes: 8 additions & 0 deletions tests/4.Utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'assert';
import * as utils from '../src/utils/utils';
import * as nacl from '../src/nacl/naclWrappers';

describe('utils', () => {
describe('concatArrays', () => {
Expand Down Expand Up @@ -33,3 +34,10 @@ describe('utils', () => {
});
});
});

describe('nacl wrapper', () => {
it('should validate signature length', () => {
assert.strictEqual(nacl.isValidSignatureLength(6), false);
assert.strictEqual(nacl.isValidSignatureLength(64), true);
});
});
Loading

0 comments on commit 9eaf7f6

Please sign in to comment.