Repository: algorand/js-algorand-sdk
Pull request #614 (2022-07-22 16:10:19)

Testing: Use Dev mode network for cucumber tests
This PR aims to allow Dev mode network to be used in cucumber tests to speed up testing time. 

Cucumber's `this` and `World` objects seem to be bound to the steps, so I refrained from using helper methods that rely on state associated with the cucumber's world/this object. This PR also removes some `statusAfterBlock` v1 API calls, as they seem to be more flaky in dev mode, and often a redundant step in the scheme of the entire cucumber scenario. 



Patch:
diff --git a/Makefile b/Makefile
index c0dbe83..f41f679 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/src/logic/logic.ts b/src/logic/logic.ts
index 9d8d60e..9f66b23 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 12d8534..9c27efa 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 5c89fcb..245ac67 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<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,
@@ -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 754f48f..91027ee 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 10825a4..4f737a0 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));


Test patch:
diff --git a/tests/4.Utils.ts b/tests/4.Utils.ts
index a06abbb..c372fed 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 d4b0e24..7dc7596 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 6ad333d..e831786 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 9947ac3..e7e7a5f 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 5f0607e..bfd192f 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);
   });