diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b1167a1..d0059da0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# v1.19.1 + +### Enhancements + +- API: Support attaching signatures to standard and multisig transactions by @jdtzmn in https://github.com/algorand/js-algorand-sdk/pull/595 +- AVM: Consolidate TEAL and AVM versions by @michaeldiamant in https://github.com/algorand/js-algorand-sdk/pull/609 +- Testing: Use Dev mode network for cucumber tests by @algochoi in https://github.com/algorand/js-algorand-sdk/pull/614 + # v1.19.0 ## What's Changed diff --git a/Makefile b/Makefile index c0dbe8307..f41f6790c 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ unit: node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.transactions.keyreg or @unit.transactions.payment or @unit.responses.231 or @unit.feetest or @unit.indexer.logs or @unit.abijson or @unit.abijson.byname or @unit.atomic_transaction_composer or @unit.responses.unlimited_assets or @unit.indexer.ledger_refactoring or @unit.algod.ledger_refactoring or @unit.dryrun.trace.application or @unit.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js integration: - node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @indexer or @rekey or @send.keyregtxn or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231 or @abi or @c2c or @compile.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js + node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @indexer or @rekey_v1 or @send.keyregtxn or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231 or @abi or @c2c or @compile.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js docker-test: ./tests/cucumber/docker/run_docker.sh diff --git a/README.md b/README.md index d28deac83..24bd04c7a 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Include a minified browser bundle directly in your HTML like so: ```html ``` @@ -32,8 +32,8 @@ or ```html ``` diff --git a/package-lock.json b/package-lock.json index 9773bb7a5..7b6856a2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "algosdk", - "version": "1.19.0", + "version": "1.19.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "algosdk", - "version": "1.19.0", + "version": "1.19.1", "license": "MIT", "dependencies": { "algo-msgpack-with-bigint": "^2.1.1", diff --git a/package.json b/package.json index 9648348b2..7a3234197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosdk", - "version": "1.19.0", + "version": "1.19.1", "description": "The official JavaScript SDK for Algorand", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/src/logic/logic.ts b/src/logic/logic.ts index 9d8d60ef6..9f66b23f6 100644 --- a/src/logic/logic.ts +++ b/src/logic/logic.ts @@ -247,9 +247,7 @@ export function readProgram( } // costs calculated dynamically starting in v4 if (version < 4 && cost > maxCost) { - throw new Error( - 'program too costly for Teal version < 4. consider using v4.' - ); + throw new Error('program too costly for version < 4. consider using v4.'); } return [ints, byteArrays, true]; } diff --git a/src/main.ts b/src/main.ts index 12d853485..9c27efa82 100644 --- a/src/main.ts +++ b/src/main.ts @@ -166,6 +166,9 @@ export { signMultisigTransaction, mergeMultisigTransactions, appendSignMultisigTransaction, + createMultisigTransaction, + appendSignRawMultisigSignature, + verifyMultisig, multisigAddress, } from './multisig'; export { SourceMap } from './logic/sourcemap'; diff --git a/src/multisig.ts b/src/multisig.ts index 5c89fcb1b..245ac6749 100644 --- a/src/multisig.ts +++ b/src/multisig.ts @@ -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; @@ -40,41 +42,27 @@ interface MultisigMetadataWithPks extends Omit { } /** - * 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, @@ -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. */ @@ -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 @@ -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 @@ -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 */ diff --git a/src/nacl/naclWrappers.ts b/src/nacl/naclWrappers.ts index 754f48ff2..91027ee0b 100644 --- a/src/nacl/naclWrappers.ts +++ b/src/nacl/naclWrappers.ts @@ -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); } diff --git a/src/transaction.ts b/src/transaction.ts index 10825a43e..4f737a037 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -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)); diff --git a/tests/4.Utils.ts b/tests/4.Utils.ts index a06abbb61..c372fed91 100644 --- a/tests/4.Utils.ts +++ b/tests/4.Utils.ts @@ -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', () => { @@ -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); + }); +}); diff --git a/tests/6.Multisig.ts b/tests/6.Multisig.ts index d4b0e2475..7dc759608 100644 --- a/tests/6.Multisig.ts +++ b/tests/6.Multisig.ts @@ -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( @@ -167,6 +168,169 @@ describe('Multisig Functionality', () => { }); }); + describe('create/append multisig with external signatures', () => { + 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 unsignedMultisigTx = algosdk.createMultisigTransaction( + decRawTx, + 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( diff --git a/tests/7.AlgoSDK.js b/tests/7.AlgoSDK.js index 6ad333d09..e831786b9 100644 --- a/tests/7.AlgoSDK.js +++ b/tests/7.AlgoSDK.js @@ -261,6 +261,7 @@ 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]); @@ -268,6 +269,71 @@ describe('Algosdk (AKA end to end)', () => { 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 + ); + }); + + it('should not attach signature with incorrect length', () => { + 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.slice(0, -1); // without the last byte + + // Check that the signature is not attached + assert.throws( + () => txn.attachSignature(signer.addr, signature), + Error('Invalid signature length') + ); + }); }); describe('Multisig Sign', () => { diff --git a/tests/8.LogicSig.ts b/tests/8.LogicSig.ts index 9947ac331..e7e7a5fba 100644 --- a/tests/8.LogicSig.ts +++ b/tests/8.LogicSig.ts @@ -872,9 +872,7 @@ describe('Program validation', () => { program[0] = oldVersions[i]; assert.throws( () => logic.checkProgram(program), - new Error( - 'program too costly for Teal version < 4. consider using v4.' - ) + new Error('program too costly for version < 4. consider using v4.') ); } // new versions @@ -884,7 +882,7 @@ describe('Program validation', () => { assert.ok(logic.checkProgram(program)); } }); - it('should support TEAL v2 opcodes', () => { + it('should support AVM v2 opcodes', () => { assert.ok(logic.langspecEvalMaxVersion >= 2); assert.ok(logic.langspecLogicSigVersion >= 2); @@ -904,7 +902,7 @@ describe('Program validation', () => { result = logic.checkProgram(program); assert.strictEqual(result, true); }); - it('should support TEAL v3 opcodes', () => { + it('should support AVM v3 opcodes', () => { assert.ok(logic.langspecEvalMaxVersion >= 3); assert.ok(logic.langspecLogicSigVersion >= 3); @@ -954,7 +952,7 @@ describe('Program validation', () => { ]); // int 0; int 1; swap; pop assert.ok(logic.checkProgram(program)); }); - it('should support TEAL v4 opcodes', () => { + it('should support AVM v4 opcodes', () => { assert.ok(logic.langspecEvalMaxVersion >= 4); // divmodw @@ -1073,7 +1071,7 @@ describe('Program validation', () => { ]); // int 1; loop: int 2; *; dup; int 10; <; bnz loop; int 16; == assert.ok(logic.checkProgram(program)); }); - it('should support TEAL v5 opcodes', () => { + it('should support AVM v5 opcodes', () => { assert.ok(logic.langspecEvalMaxVersion >= 5); // itxn ops @@ -1100,7 +1098,7 @@ describe('Program validation', () => { // byte "a"; byte "b"; byte "c"; cover 2; uncover 2; concat; concat; log; int 1 assert.ok(logic.checkProgram(program)); }); - it('should support TEAL v6 opcodes', () => { + it('should support AVM v6 opcodes', () => { assert.ok(logic.langspecEvalMaxVersion >= 6); // bsqrt op diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index 5f0607ee5..bfd192f5c 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -125,6 +125,20 @@ module.exports = function getSteps(options) { steps.then[name] = fn; } + // Dev Mode State + const DEV_MODE_INITIAL_MICROALGOS = 10_000_000; + + /* + * waitForAlgodInDevMode is a Dev mode helper method that waits for a transaction to resolve. + * Since Dev mode produces blocks on a per transaction basis, it's possible + * algod generates a block _before_ the corresponding SDK call to wait for a block. + * Without _any_ wait, it's possible the SDK looks for the transaction before algod completes processing. + * So, the method performs a local sleep to simulate waiting for a block. + */ + function waitForAlgodInDevMode() { + return new Promise((resolve) => setTimeout(resolve, 500)); + } + const { algod_token: algodToken, kmd_token: kmdToken } = options; Given('an algod client', async function () { @@ -185,6 +199,26 @@ module.exports = function getSteps(options) { }); When('I get status after this block', async function () { + // Send a transaction to advance blocks in dev mode. + const sp = await this.acl.getTransactionParams(); + if (sp.firstRound === 0) sp.firstRound = 1; + const fundingTxnArgs = { + from: this.accounts[0], + to: this.accounts[0], + amount: 0, + fee: sp.fee, + firstRound: sp.lastRound + 1, + lastRound: sp.lastRound + 1000, + genesisHash: sp.genesishashb64, + genesisID: sp.genesisID, + }; + const stxKmd = await this.kcl.signTransaction( + this.handle, + this.wallet_pswd, + fundingTxnArgs + ); + await this.acl.sendRawTransaction(stxKmd); + this.statusAfter = await this.acl.statusAfterBlock(this.status.lastRound); return this.statusAfter; }); @@ -359,6 +393,35 @@ module.exports = function getSteps(options) { return this.pk; }); + When( + 'I generate a key using kmd for rekeying and fund it', + async function () { + this.rekey = await this.kcl.generateKey(this.handle); + this.rekey = this.rekey.address; + // Fund the rekey address with some Algos + const sp = await this.acl.getTransactionParams(); + if (sp.firstRound === 0) sp.firstRound = 1; + const fundingTxnArgs = { + from: this.accounts[0], + to: this.rekey, + amount: DEV_MODE_INITIAL_MICROALGOS, + fee: sp.fee, + firstRound: sp.lastRound + 1, + lastRound: sp.lastRound + 1000, + genesisHash: sp.genesishashb64, + genesisID: sp.genesisID, + }; + + const stxKmd = await this.kcl.signTransaction( + this.handle, + this.wallet_pswd, + fundingTxnArgs + ); + await this.acl.sendRawTransaction(stxKmd); + return this.rekey; + } + ); + Then('the key should be in the wallet', async function () { let keys = await this.kcl.listKeys(this.handle); keys = keys.addresses; @@ -431,6 +494,27 @@ module.exports = function getSteps(options) { } ); + Given( + 'default transaction with parameters {int} {string} and rekeying key', + async function (amt, note) { + this.pk = this.rekey; + const result = await this.acl.getTransactionParams(); + this.lastRound = result.lastRound; + this.txn = { + from: this.rekey, + to: this.accounts[1], + fee: result.fee, + firstRound: result.lastRound + 1, + lastRound: result.lastRound + 1000, + genesisHash: result.genesishashb64, + genesisID: result.genesisID, + note: makeUint8Array(Buffer.from(note, 'base64')), + amount: parseInt(amt), + }; + return this.txn; + } + ); + Given( 'default multisig transaction with parameters {int} {string}', async function (amt, note) { @@ -801,13 +885,13 @@ module.exports = function getSteps(options) { assert.deepStrictEqual(true, 'type' in info); // let localParams = await this.acl.getTransactionParams(); // this.lastRound = localParams.lastRound; - await this.acl.statusAfterBlock(this.lastRound + 2); + await waitForAlgodInDevMode(); info = await this.acl.transactionById(this.txid); assert.deepStrictEqual(true, 'type' in info); }); Then('I can get the transaction by ID', async function () { - await this.acl.statusAfterBlock(this.lastRound + 2); + await waitForAlgodInDevMode(); const info = await this.acl.transactionById(this.txid); assert.deepStrictEqual(true, 'type' in info); }); @@ -3900,7 +3984,7 @@ module.exports = function getSteps(options) { const info = await algosdk.waitForConfirmation( this.v2Client, fundingResponse.txId, - 2 + 1 ); assert.ok(info['confirmed-round'] > 0); } @@ -4256,7 +4340,7 @@ module.exports = function getSteps(options) { const info = await algosdk.waitForConfirmation( this.v2Client, fundingResponse.txId, - 2 + 1 ); assert.ok(info['confirmed-round'] > 0); } @@ -4377,7 +4461,7 @@ module.exports = function getSteps(options) { const info = await algosdk.waitForConfirmation( this.v2Client, this.appTxid.txId, - 2 + 1 ); assert.ok(info['confirmed-round'] > 0); });