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

API: Support attaching signatures to standard and multisig transactions #595

Merged
merged 10 commits into from
Jul 28, 2022
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,
createRawMultisigTransaction,
appendSignRawMultisigSignature,
verifyMultisig,
multisigAddress,
} from './multisig';
export const LogicTemplates = LogicTemplatesCommonJSExport.default;
Expand Down
111 changes: 108 additions & 3 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 @@ -45,7 +47,7 @@ interface MultisigMetadataWithPks extends Omit<MultisigMetadata, 'addrs'> {
* @param rawSig - a Buffer raw signature of that transaction
* @param myPk - a public key that corresponds with rawSig
* @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
*/
Expand Down Expand Up @@ -97,6 +99,49 @@ function createMultisigTransaction(
return new Uint8Array(encoding.encode(signedTxn));
}

/**
* createRawMultisigTransaction creates a raw, unsigned multisig transaction blob.
* @param txnForEncoding - the actual transaction.
* @param version - multisig version
* @param threshold - multisig threshold
* @param pks - ordered list of public keys in this multisig
* @returns encoded multisig blob
*/
export function createRawMultisigTransaction(
txnForEncoding: EncodedTransaction,
{ version, threshold, addrs }: MultisigMetadata
) {
// construct the appendable multisigned transaction format
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 signedTxn: EncodedSignedTransaction = {
msig,
txn: txnForEncoding,
};

// 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(txnForEncoding.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 @@ -147,6 +192,32 @@ export class MultisigTransaction extends txnBuilder.Transaction {
);
}

/**
* 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 createMultisigTransaction(
this.get_obj_for_encoding(),
{
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 +383,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 +462,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);
});
});
168 changes: 168 additions & 0 deletions tests/6.Multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
MultisigTransaction,
MULTISIG_NO_MUTATE_ERROR_MSG,
MULTISIG_USE_PARTIAL_SIGN_ERROR_MSG,
MULTISIG_SIGNATURE_LENGTH_ERROR_MSG,
} from '../src/multisig';

const sampleAccount1 = algosdk.mnemonicToSecretKey(
Expand Down Expand Up @@ -167,6 +168,173 @@ describe('Multisig Functionality', () => {
});
});

describe('appendMultisigTransactionSignature', () => {
it('should match golden main repo result', () => {
const oneSigTxn = Buffer.from(
'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAuLAFE0oma0skOoAmOzEwfPuLYpEWl4LINtsiLrUqWQkDxh4WHb29//YCpj4MFbiSgD2jKYt0XKRD86zKCF4RDYGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AaN0eG6Lo2FtdM0D6KVjbG9zZcQgQOk0koglZMvOnFmmm2dUJonpocOiqepbZabopEIf/FejZmVlzQPoomZ2zfMVo2dlbqxkZXZuZXQtdjM4LjCiZ2jEIP6zbDkQFDkAw9pVQsoYNrAP0vgZWRJXzSP2BC+YyDadomx2zfb9pG5vdGXECEUmIgAYUob7o3JjdsQge2ziT+tbrMCxZOKcIixX9fY9w4fUOQSCWEEcX+EPfAKjc25kxCCNkrSJkAFzoE36Q1mjZmpq/OosQqBd2cH3PuulR4A36aR0eXBlo3BheQ==',
'base64'
);

const signerAddr = sampleAccount2.addr;
const signedTxn = algosdk.appendSignMultisigTransaction(
oneSigTxn,
sampleMultisigParams,
sampleAccount2.sk
);

const multisig = algosdk.decodeSignedTransaction(signedTxn.blob).msig;
if (multisig === undefined) {
throw new Error('multisig is undefined');
}

const signatures = multisig.subsig;
if (signatures === undefined) {
throw new Error('No signatures found');
}

const signature = signatures[1].s;
if (signature === undefined) {
throw new Error('No signature found');
}

const { txID, blob } = algosdk.appendSignRawMultisigSignature(
oneSigTxn,
sampleMultisigParams,
signerAddr,
signature
);

const expectedTxID =
'MANN3ESOHQVHFZBAGD6UK6XFVWEFZQJPWO5SQ2J5LZRCF5E2VVQQ';
assert.strictEqual(txID, expectedTxID);

const expectedBlob = Buffer.from(
'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAuLAFE0oma0skOoAmOzEwfPuLYpEWl4LINtsiLrUqWQkDxh4WHb29//YCpj4MFbiSgD2jKYt0XKRD86zKCF4RDYKicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxoXPEQBAhuyRjsOrnHp3s/xI+iMKiL7QPsh8iJZ22YOJJP0aFUwedMr+a6wfdBXk1OefyrAN1wqJ9rq6O+DrWV1fH0ASBonBrxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaN0aHICoXYBo3R4boujYW10zQPopWNsb3NlxCBA6TSSiCVky86cWaabZ1Qmiemhw6Kp6ltlpuikQh/8V6NmZWXNA+iiZnbN8xWjZ2VurGRldm5ldC12MzguMKJnaMQg/rNsORAUOQDD2lVCyhg2sA/S+BlZElfNI/YEL5jINp2ibHbN9v2kbm90ZcQIRSYiABhShvujcmN2xCB7bOJP61uswLFk4pwiLFf19j3Dh9Q5BIJYQRxf4Q98AqNzbmTEII2StImQAXOgTfpDWaNmamr86ixCoF3Zwfc+66VHgDfppHR5cGWjcGF5',
'base64'
);

assert.deepStrictEqual(Buffer.from(blob), expectedBlob);
});

it('should not sign with signature of invalid length', () => {
const oneSigTxn = Buffer.from(
'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAuLAFE0oma0skOoAmOzEwfPuLYpEWl4LINtsiLrUqWQkDxh4WHb29//YCpj4MFbiSgD2jKYt0XKRD86zKCF4RDYGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AaN0eG6Lo2FtdM0D6KVjbG9zZcQgQOk0koglZMvOnFmmm2dUJonpocOiqepbZabopEIf/FejZmVlzQPoomZ2zfMVo2dlbqxkZXZuZXQtdjM4LjCiZ2jEIP6zbDkQFDkAw9pVQsoYNrAP0vgZWRJXzSP2BC+YyDadomx2zfb9pG5vdGXECEUmIgAYUob7o3JjdsQge2ziT+tbrMCxZOKcIixX9fY9w4fUOQSCWEEcX+EPfAKjc25kxCCNkrSJkAFzoE36Q1mjZmpq/OosQqBd2cH3PuulR4A36aR0eXBlo3BheQ==',
'base64'
);

const signerAddr = sampleAccount2.addr;
const signedTxn = algosdk.appendSignMultisigTransaction(
oneSigTxn,
sampleMultisigParams,
sampleAccount2.sk
);

const multisig = algosdk.decodeSignedTransaction(signedTxn.blob).msig;
if (multisig === undefined) {
throw new Error('multisig is undefined');
}

const signatures = multisig.subsig;
if (signatures === undefined) {
throw new Error('No signatures found');
}

const signature = signatures[1].s;
if (signature === undefined) {
throw new Error('No signature found');
}

// Remove the last byte of the signature
const invalidSignature = signature.slice(0, -1);
assert.throws(
() =>
algosdk.appendSignRawMultisigSignature(
oneSigTxn,
sampleMultisigParams,
signerAddr,
invalidSignature
),
Error(MULTISIG_SIGNATURE_LENGTH_ERROR_MSG)
);
});

it('should append signature to created raw multisig transaction', () => {
const rawTxBlob = Buffer.from(
'jKNmZWXOAAPIwKJmds4ADvnao2dlbqxkZXZuZXQtdjM4LjCiZ2jEIP6zbDkQFDkAw9pVQsoYNrAP0vgZWRJXzSP2BC+YyDadomx2zgAO/cKmc2Vsa2V5xCAyEisr1j3cUzGWF6WqU8Sxwm/j3MryjTYitWl3oUBchqNzbmTEII2StImQAXOgTfpDWaNmamr86ixCoF3Zwfc+66VHgDfppHR5cGWma2V5cmVnp3ZvdGVmc3TOAA27oKZ2b3Rla2TNJxCndm90ZWtlecQgcBvX+5ErB7MIEf8oHZ/ulWPlgC4gJokjGSWPd/qTHoindm90ZWxzdM4AD0JA',
'base64'
);
const decRawTx = algosdk.decodeUnsignedTransaction(rawTxBlob);
const encodedRawTx = decRawTx.get_obj_for_encoding();
if (encodedRawTx === undefined) {
throw new Error('encodedRawTx is undefined');
}

const unsignedMultisigTx = algosdk.createRawMultisigTransaction(
encodedRawTx,
sampleMultisigParams
);

// Check that the unsignedMultisigTx is valid
interface ExpectedMultisigTxStructure {
msig: {
subsig: {
pk: Uint8Array;
s: Uint8Array;
}[];
};
}
const unsignedMultisigTxBlob = algosdk.decodeObj(
unsignedMultisigTx
) as ExpectedMultisigTxStructure;
assert.deepStrictEqual(
unsignedMultisigTxBlob.msig.subsig[0].pk,
algosdk.decodeAddress(sampleAccount1.addr).publicKey
);
assert.strictEqual(unsignedMultisigTxBlob.msig.subsig[1].s, undefined);

// Sign the raw transaction with a signature generated from the first account
const signerAddr = sampleAccount1.addr;
const signedTxn = algosdk.appendSignMultisigTransaction(
unsignedMultisigTx,
sampleMultisigParams,
sampleAccount1.sk
);

const multisig = algosdk.decodeSignedTransaction(signedTxn.blob).msig;
if (multisig === undefined) {
throw new Error('multisig is undefined');
}

const signatures = multisig.subsig;
if (signatures === undefined) {
throw new Error('No signatures found');
}

const signature = signatures[0].s;
if (signature === undefined) {
throw new Error('No signature found');
}
const { txID, blob } = algosdk.appendSignRawMultisigSignature(
unsignedMultisigTx,
sampleMultisigParams,
signerAddr,
signature
);

// Check that the signed raw multisig is valid
const expectedTxID =
'E7DA7WTJCWWFQMKSVU5HOIJ5F5HGVGMOZGBIHRJRYGIX7FIJ5VWA';
assert.strictEqual(txID, expectedTxID);

const expectedBlob = Buffer.from(
'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAcT0s17wJbvnza+NpyHwM0RWbQ+HwKmsT1PLs+w6d6MpdTH3tra+yKZE0K0qEyhSE7Y56+B9oaf2orEbjc/njDYGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AaN0eG6Mo2ZlZc4AA8jAomZ2zgAO+dqjZ2VurGRldm5ldC12MzguMKJnaMQg/rNsORAUOQDD2lVCyhg2sA/S+BlZElfNI/YEL5jINp2ibHbOAA79wqZzZWxrZXnEIDISKyvWPdxTMZYXpapTxLHCb+PcyvKNNiK1aXehQFyGo3NuZMQgjZK0iZABc6BN+kNZo2ZqavzqLEKgXdnB9z7rpUeAN+mkdHlwZaZrZXlyZWendm90ZWZzdM4ADbugpnZvdGVrZM0nEKd2b3Rla2V5xCBwG9f7kSsHswgR/ygdn+6VY+WALiAmiSMZJY93+pMeiKd2b3RlbHN0zgAPQkA=',
'base64'
);

assert.deepStrictEqual(Buffer.from(blob), expectedBlob);
});
});

describe('should sign keyreg transaction types', () => {
it('first partial sig should match golden main repo result', () => {
const rawTxBlob = Buffer.from(
Expand Down
Loading