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
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export {
signMultisigTransaction,
mergeMultisigTransactions,
appendSignMultisigTransaction,
appendSignRawMultisigSignature,
verifyMultisig,
multisigAddress,
} from './multisig';
export const LogicTemplates = LogicTemplatesCommonJSExport.default;
Expand Down
56 changes: 55 additions & 1 deletion src/multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ 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
) {
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 +332,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,6 +411,40 @@ 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
Expand Down
13 changes: 13 additions & 0 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,19 @@ export class Transaction implements TransactionStorageStructure {
return new Uint8Array(encoding.encode(sTxn));
}

attachSignature(signerAddr: string, signature: Uint8Array) {
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
48 changes: 48 additions & 0 deletions tests/6.Multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,54 @@ 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);
});
});

describe('should sign keyreg transaction types', () => {
it('first partial sig should match golden main repo result', () => {
const rawTxBlob = Buffer.from(
Expand Down
36 changes: 36 additions & 0 deletions tests/7.AlgoSDK.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,13 +261,49 @@ describe('Algosdk (AKA end to end)', () => {
const signed = algosdk.signBytes(toSign, account.sk);
assert.equal(true, algosdk.verifyBytes(toSign, signed, account.addr));
});

it('should not verify a corrupted signature', () => {
const account = algosdk.generateAccount();
const toSign = Buffer.from([1, 9, 25, 49]);
const signed = algosdk.signBytes(toSign, account.sk);
signed[0] = (signed[0] + 1) % 256;
assert.equal(false, algosdk.verifyBytes(toSign, signed, account.addr));
});

it('should attach arbitrary signatures', () => {
const sender = algosdk.generateAccount();
const signer = algosdk.generateAccount();

// Create a transaction
const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
from: sender.addr,
to: signer.addr,
amount: 1000,
suggestedParams: {
firstRound: 12466,
lastRound: 13466,
genesisID: 'devnet-v33.0',
genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
fee: 4,
},
});

// Sign it directly to get a signature
const signedWithSk = txn.signTxn(signer.sk);
const decoded = algosdk.decodeObj(signedWithSk);
const signature = decoded.sig;

// Attach the signature to the transaction indirectly, and compare
const signedWithSignature = txn.attachSignature(signer.addr, signature);
assert.deepEqual(signedWithSk, signedWithSignature);

// Check that signer was set
const decodedWithSigner = algosdk.decodeObj(signedWithSignature);
assert.deepEqual(
decodedWithSigner.sgnr,
algosdk.decodeAddress(signer.addr).publicKey
);
});
});

describe('Multisig Sign', () => {
Expand Down