Repository: algorand/js-algorand-sdk
Pull request #629 (2022-08-22 20:32:30)

enhancement: Initial stateproofs support
Initial support for state proofs APIs in the JS Client.

`src/client/v2/algod/models/types.ts` was generated using the algorand/generator package.

Eric-Warehime: > I don't see the endpoint `/v2/blocks/{round}/transactions/{txid}/proof` added in the PR

It was added previously https://github.com/algorand/js-algorand-sdk/blob/develop/src/client/v2/algod/proof.ts#L19
Eric-Warehime: > Overall looks pretty good. Need to keep in mind that state proofs introduce a new transaction. Currently, the msgpack decoder would fail if it comes across a state proof transaction. Think we should add basic support for the state proof transaction - load the state proof message and the proof itself into a byte array (similar to what we do on the python sdk)

Added message, type, and proof to the txn. The python leaves out the added block header fields, so I've done the same here.
algoidan: looking at the code, it looks like the `js` code does define a block header structure (https://github.com/algorand/js-algorand-sdk/blob/develop/src/types/blockHeader.ts)
should we add the state-proof tracking + the txn256?
Eric-Warehime: > looking at the code, it looks like the `js` code does define a block header structure (https://github.com/algorand/js-algorand-sdk/blob/develop/src/types/blockHeader.ts) should we add the `state-proof tracking` + `txn256` to it?

Added.

Patch:
diff --git a/src/client/v2/algod/algod.ts b/src/client/v2/algod/algod.ts
index c9d4b2b..c64bb49 100644
--- a/src/client/v2/algod/algod.ts
+++ b/src/client/v2/algod/algod.ts
@@ -6,25 +6,27 @@ import AccountApplicationInformation from './accountApplicationInformation';
 import Block from './block';
 import Compile from './compile';
 import Dryrun from './dryrun';
+import Genesis from './genesis';
 import GetAssetByID from './getAssetByID';
 import GetApplicationByID from './getApplicationByID';
 import HealthCheck from './healthCheck';
 import PendingTransactionInformation from './pendingTransactionInformation';
 import PendingTransactions from './pendingTransactions';
 import PendingTransactionsByAddress from './pendingTransactionsByAddress';
+import GetTransactionProof from './getTransactionProof';
 import SendRawTransaction from './sendRawTransaction';
 import Status from './status';
 import StatusAfterBlock from './statusAfterBlock';
 import SuggestedParams from './suggestedParams';
 import Supply from './supply';
 import Versions from './versions';
-import Genesis from './genesis';
-import Proof from './proof';
 import { BaseHTTPClient } from '../../baseHTTPClient';
 import {
   AlgodTokenHeader,
   CustomTokenHeader,
 } from '../../urlTokenBaseHTTPClient';
+import LightBlockHeaderProof from './lightBlockHeaderProof';
+import StateProof from './stateproof';
 
 /**
  * Algod client connects an application to the Algorand blockchain. The algod client requires a valid algod REST endpoint IP address and algod token from an Algorand node that is connected to the network you plan to interact with.
@@ -451,7 +453,7 @@ export default class AlgodClient extends ServiceClient {
    * ```typescript
    * const round = 18038133;
    * const txId = "MEUOC4RQJB23CQZRFRKYEI6WBO73VTTPST5A7B3S5OKBUY6LFUDA";
-   * const proof = await algodClient.getProof(round, txId).do();
+   * const proof = await algodClient.getTransactionProof(round, txId).do();
    * ```
    *
    * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2/#get-v2blocksroundtransactionstxidproof)
@@ -459,7 +461,39 @@ export default class AlgodClient extends ServiceClient {
    * @param txID - The transaction ID for which to generate a proof.
    * @category GET
    */
-  getProof(round: number, txID: string) {
-    return new Proof(this.c, this.intDecoding, round, txID);
+  getTransactionProof(round: number, txID: string) {
+    return new GetTransactionProof(this.c, this.intDecoding, round, txID);
+  }
+
+  /**
+   * Gets a proof for a given light block header inside a state proof commitment.
+   *
+   * #### Example
+   * ```typescript
+   * const round = 11111111;
+   * const lightBlockHeaderProof = await algodClient.getLightBlockHeaderProof(round).do();
+   * ```
+   *
+   * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2#get-v2blocksroundlightheaderproof)
+   * @param round
+   */
+  getLightBlockHeaderProof(round: number) {
+    return new LightBlockHeaderProof(this.c, this.intDecoding, round);
+  }
+
+  /**
+   * Gets a state proof that covers a given round.
+   *
+   * #### Example
+   * ```typescript
+   * const round = 11111111;
+   * const stateProof = await algodClient.getStateProof(round).do();
+   * ```
+   *
+   * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2#get-v2stateproofsround)
+   * @param round
+   */
+  getStateProof(round: number) {
+    return new StateProof(this.c, this.intDecoding, round);
   }
 }
diff --git a/src/client/v2/algod/getTransactionProof.ts b/src/client/v2/algod/getTransactionProof.ts
new file mode 100644
index 0000000..398039a
--- /dev/null
+++ b/src/client/v2/algod/getTransactionProof.ts
@@ -0,0 +1,43 @@
+import JSONRequest from '../jsonrequest';
+import HTTPClient from '../../client';
+import IntDecoding from '../../../types/intDecoding';
+
+export default class GetTransactionProof extends JSONRequest {
+  constructor(
+    c: HTTPClient,
+    intDecoding: IntDecoding,
+    private round: number,
+    private txID: string
+  ) {
+    super(c, intDecoding);
+
+    this.round = round;
+    this.txID = txID;
+  }
+
+  path() {
+    return `/v2/blocks/${this.round}/transactions/${this.txID}/proof`;
+  }
+
+  /**
+   * Exclude assets and application data from results
+   * The type of hash function used to create the proof, must be one of: "sha512_256", "sha256"
+   *
+   * #### Example
+   * ```typescript
+   * const hashType = "sha256";
+   * const round = 123456;
+   * const txId = "abc123;
+   * const txProof = await algodClient.getTransactionProof(round, txId)
+   *        .hashType(hashType)
+   *        .do();
+   * ```
+   *
+   * @param hashType
+   * @category query
+   */
+  hashType(hashType: string) {
+    this.query.hashtype = hashType;
+    return this;
+  }
+}
diff --git a/src/client/v2/algod/lightBlockHeaderProof.ts b/src/client/v2/algod/lightBlockHeaderProof.ts
new file mode 100644
index 0000000..ac3794c
--- /dev/null
+++ b/src/client/v2/algod/lightBlockHeaderProof.ts
@@ -0,0 +1,15 @@
+import JSONRequest from '../jsonrequest';
+import HTTPClient from '../../client';
+import IntDecoding from '../../../types/intDecoding';
+
+export default class LightBlockHeaderProof extends JSONRequest {
+  constructor(c: HTTPClient, intDecoding: IntDecoding, private round: number) {
+    super(c, intDecoding);
+
+    this.round = round;
+  }
+
+  path() {
+    return `/v2/blocks/${this.round}/lightheader/proof`;
+  }
+}
diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts
index d91d84c..f5d0fbf 100644
--- a/src/client/v2/algod/models/types.ts
+++ b/src/client/v2/algod/models/types.ts
@@ -1631,6 +1631,54 @@ export class EvalDeltaKeyValue extends BaseModel {
   }
 }
 
+/**
+ * Proof of membership and position of a light block header.
+ */
+export class LightBlockHeaderProof extends BaseModel {
+  /**
+   * The index of the light block header in the vector commitment tree
+   */
+  public index: number | bigint;
+
+  /**
+   * The encoded proof.
+   */
+  public proof: Uint8Array;
+
+  /**
+   * Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   */
+  public treedepth: number | bigint;
+
+  /**
+   * Creates a new `LightBlockHeaderProof` object.
+   * @param index - The index of the light block header in the vector commitment tree
+   * @param proof - The encoded proof.
+   * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   */
+  constructor(
+    index: number | bigint,
+    proof: string | Uint8Array,
+    treedepth: number | bigint
+  ) {
+    super();
+    this.index = index;
+    this.proof =
+      typeof proof === 'string'
+        ? new Uint8Array(Buffer.from(proof, 'base64'))
+        : proof;
+    this.treedepth = treedepth;
+
+    this.attribute_map = {
+      index: 'index',
+      proof: 'proof',
+      treedepth: 'treedepth',
+    };
+  }
+}
+
 /**
  *
  */
@@ -2038,80 +2086,112 @@ export class PostTransactionsResponse extends BaseModel {
 }
 
 /**
- * Proof of transaction in a block.
+ * Represents a state proof and its corresponding message
  */
-export class ProofResponse extends BaseModel {
+export class StateProof extends BaseModel {
   /**
-   * Index of the transaction in the block's payset.
+   * Represents the message that the state proofs are attesting to.
    */
-  public idx: number | bigint;
+  public message: StateProofMessage;
 
   /**
-   * Merkle proof of transaction membership.
+   * The encoded StateProof for the message.
    */
-  public proof: Uint8Array;
+  public stateproof: Uint8Array;
 
   /**
-   * Hash of SignedTxnInBlock for verifying proof.
+   * Creates a new `StateProof` object.
+   * @param message - Represents the message that the state proofs are attesting to.
+   * @param stateproof - The encoded StateProof for the message.
    */
-  public stibhash: Uint8Array;
+  constructor(message: StateProofMessage, stateproof: string | Uint8Array) {
+    super();
+    this.message = message;
+    this.stateproof =
+      typeof stateproof === 'string'
+        ? new Uint8Array(Buffer.from(stateproof, 'base64'))
+        : stateproof;
+
+    this.attribute_map = {
+      message: 'Message',
+      stateproof: 'StateProof',
+    };
+  }
+}
 
+/**
+ * Represents the message that the state proofs are attesting to.
+ */
+export class StateProofMessage extends BaseModel {
   /**
-   * Represents the depth of the tree that is being proven, i.e. the number of edges
-   * from a leaf to the root.
+   * The vector commitment root on all light block headers within a state proof
+   * interval.
    */
-  public treedepth: number | bigint;
+  public blockheaderscommitment: Uint8Array;
 
   /**
-   * The type of hash function used to create the proof, must be one of:
-   * * sha512_256
-   * * sha256
+   * The first round the message attests to.
    */
-  public hashtype?: string;
+  public firstattestedround: number | bigint;
 
   /**
-   * Creates a new `ProofResponse` object.
-   * @param idx - Index of the transaction in the block's payset.
-   * @param proof - Merkle proof of transaction membership.
-   * @param stibhash - Hash of SignedTxnInBlock for verifying proof.
-   * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges
-   * from a leaf to the root.
-   * @param hashtype - The type of hash function used to create the proof, must be one of:
-   * * sha512_256
-   * * sha256
+   * The last round the message attests to.
+   */
+  public lastattestedround: number | bigint;
+
+  /**
+   * An integer value representing the natural log of the proven weight with 16 bits
+   * of precision. This value would be used to verify the next state proof.
+   */
+  public lnprovenweight: number | bigint;
+
+  /**
+   * The vector commitment root of the top N accounts to sign the next StateProof.
+   */
+  public voterscommitment: Uint8Array;
+
+  /**
+   * Creates a new `StateProofMessage` object.
+   * @param blockheaderscommitment - The vector commitment root on all light block headers within a state proof
+   * interval.
+   * @param firstattestedround - The first round the message attests to.
+   * @param lastattestedround - The last round the message attests to.
+   * @param lnprovenweight - An integer value representing the natural log of the proven weight with 16 bits
+   * of precision. This value would be used to verify the next state proof.
+   * @param voterscommitment - The vector commitment root of the top N accounts to sign the next StateProof.
    */
   constructor({
-    idx,
-    proof,
-    stibhash,
-    treedepth,
-    hashtype,
+    blockheaderscommitment,
+    firstattestedround,
+    lastattestedround,
+    lnprovenweight,
+    voterscommitment,
   }: {
-    idx: number | bigint;
-    proof: string | Uint8Array;
-    stibhash: string | Uint8Array;
-    treedepth: number | bigint;
-    hashtype?: string;
+    blockheaderscommitment: string | Uint8Array;
+    firstattestedround: number | bigint;
+    lastattestedround: number | bigint;
+    lnprovenweight: number | bigint;
+    voterscommitment: string | Uint8Array;
   }) {
     super();
-    this.idx = idx;
-    this.proof =
-      typeof proof === 'string'
-        ? new Uint8Array(Buffer.from(proof, 'base64'))
-        : proof;
-    this.stibhash =
-      typeof stibhash === 'string'
-        ? new Uint8Array(Buffer.from(stibhash, 'base64'))
-        : stibhash;
-    this.treedepth = treedepth;
-    this.hashtype = hashtype;
+    this.blockheaderscommitment =
+      typeof blockheaderscommitment === 'string'
+        ? new Uint8Array(Buffer.from(blockheaderscommitment, 'base64'))
+        : blockheaderscommitment;
+    this.firstattestedround = firstattestedround;
+    this.lastattestedround = lastattestedround;
+    this.lnprovenweight = lnprovenweight;
+    this.voterscommitment =
+      typeof voterscommitment === 'string'
+        ? new Uint8Array(Buffer.from(voterscommitment, 'base64'))
+        : voterscommitment;
 
     this.attribute_map = {
-      idx: 'idx',
-      proof: 'proof',
-      stibhash: 'stibhash',
-      treedepth: 'treedepth',
-      hashtype: 'hashtype',
+      blockheaderscommitment: 'BlockHeadersCommitment',
+      firstattestedround: 'FirstAttestedRound',
+      lastattestedround: 'LastAttestedRound',
+      lnprovenweight: 'LnProvenWeight',
+      voterscommitment: 'VotersCommitment',
     };
   }
 }
@@ -2317,6 +2397,85 @@ export class TransactionParametersResponse extends BaseModel {
   }
 }
 
+/**
+ * Proof of transaction in a block.
+ */
+export class TransactionProofResponse extends BaseModel {
+  /**
+   * Index of the transaction in the block's payset.
+   */
+  public idx: number | bigint;
+
+  /**
+   * Proof of transaction membership.
+   */
+  public proof: Uint8Array;
+
+  /**
+   * Hash of SignedTxnInBlock for verifying proof.
+   */
+  public stibhash: Uint8Array;
+
+  /**
+   * Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   */
+  public treedepth: number | bigint;
+
+  /**
+   * The type of hash function used to create the proof, must be one of:
+   * * sha512_256
+   * * sha256
+   */
+  public hashtype?: string;
+
+  /**
+   * Creates a new `TransactionProofResponse` object.
+   * @param idx - Index of the transaction in the block's payset.
+   * @param proof - Proof of transaction membership.
+   * @param stibhash - Hash of SignedTxnInBlock for verifying proof.
+   * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   * @param hashtype - The type of hash function used to create the proof, must be one of:
+   * * sha512_256
+   * * sha256
+   */
+  constructor({
+    idx,
+    proof,
+    stibhash,
+    treedepth,
+    hashtype,
+  }: {
+    idx: number | bigint;
+    proof: string | Uint8Array;
+    stibhash: string | Uint8Array;
+    treedepth: number | bigint;
+    hashtype?: string;
+  }) {
+    super();
+    this.idx = idx;
+    this.proof =
+      typeof proof === 'string'
+        ? new Uint8Array(Buffer.from(proof, 'base64'))
+        : proof;
+    this.stibhash =
+      typeof stibhash === 'string'
+        ? new Uint8Array(Buffer.from(stibhash, 'base64'))
+        : stibhash;
+    this.treedepth = treedepth;
+    this.hashtype = hashtype;
+
+    this.attribute_map = {
+      idx: 'idx',
+      proof: 'proof',
+      stibhash: 'stibhash',
+      treedepth: 'treedepth',
+      hashtype: 'hashtype',
+    };
+  }
+}
+
 /**
  * algod version information.
  */
diff --git a/src/client/v2/algod/proof.ts b/src/client/v2/algod/proof.ts
deleted file mode 100644
index a8b78e2..0000000
--- a/src/client/v2/algod/proof.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import JSONRequest from '../jsonrequest';
-import HTTPClient from '../../client';
-import IntDecoding from '../../../types/intDecoding';
-
-export default class Proof extends JSONRequest {
-  constructor(
-    c: HTTPClient,
-    intDecoding: IntDecoding,
-    private round: number,
-    private txID: string
-  ) {
-    super(c, intDecoding);
-
-    this.round = round;
-    this.txID = txID;
-  }
-
-  path() {
-    return `/v2/blocks/${this.round}/transactions/${this.txID}/proof`;
-  }
-}
diff --git a/src/client/v2/algod/stateproof.ts b/src/client/v2/algod/stateproof.ts
new file mode 100644
index 0000000..98bab12
--- /dev/null
+++ b/src/client/v2/algod/stateproof.ts
@@ -0,0 +1,15 @@
+import JSONRequest from '../jsonrequest';
+import HTTPClient from '../../client';
+import IntDecoding from '../../../types/intDecoding';
+
+export default class StateProof extends JSONRequest {
+  constructor(c: HTTPClient, intDecoding: IntDecoding, private round: number) {
+    super(c, intDecoding);
+
+    this.round = round;
+  }
+
+  path() {
+    return `/v2/stateproofs/${this.round}`;
+  }
+}
diff --git a/src/client/v2/indexer/lookupAccountTransactions.ts b/src/client/v2/indexer/lookupAccountTransactions.ts
index 69f41cd..b2343f1 100644
--- a/src/client/v2/indexer/lookupAccountTransactions.ts
+++ b/src/client/v2/indexer/lookupAccountTransactions.ts
@@ -76,7 +76,7 @@ export default class LookupAccountTransactions extends JSONRequest {
    *        .do();
    * ```
    *
-   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`
+   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`, `stpf`
    * @category query
    */
   txType(type: string) {
diff --git a/src/client/v2/indexer/searchForTransactions.ts b/src/client/v2/indexer/searchForTransactions.ts
index 5afa84c..7583059 100644
--- a/src/client/v2/indexer/searchForTransactions.ts
+++ b/src/client/v2/indexer/searchForTransactions.ts
@@ -52,7 +52,7 @@ export default class SearchForTransactions extends JSONRequest {
    *        .do();
    * ```
    *
-   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`
+   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`, `stpf`
    * @category query
    */
   txType(type: string) {
diff --git a/src/transaction.ts b/src/transaction.ts
index 4f737a0..5d4f9d6 100644
--- a/src/transaction.ts
+++ b/src/transaction.ts
@@ -110,6 +110,9 @@ interface TransactionStorageStructure
   nonParticipation?: boolean;
   group?: Buffer;
   extraPages?: number;
+  stateProofType?: number | bigint;
+  stateProof?: Uint8Array;
+  stateProofMessage?: Uint8Array;
 }
 
 function getKeyregKey(
@@ -198,6 +201,9 @@ export class Transaction implements TransactionStorageStructure {
   nonParticipation?: boolean;
   group?: Buffer;
   extraPages?: number;
+  stateProofType?: number | bigint;
+  stateProof?: Uint8Array;
+  stateProofMessage?: Uint8Array;
 
   constructor({ ...transaction }: AnyTransaction) {
     // Populate defaults
@@ -528,6 +534,27 @@ export class Transaction implements TransactionStorageStructure {
 
     // say we are aware of groups
     this.group = undefined;
+
+    // stpf fields
+    if (
+      txn.stateProofType !== undefined &&
+      (!Number.isSafeInteger(txn.stateProofType) || txn.stateProofType < 0)
+    )
+      throw Error(
+        'State Proof type must be a positive number and smaller than 2^53-1'
+      );
+    if (txn.stateProofMessage !== undefined) {
+      if (txn.stateProofMessage.constructor !== Uint8Array)
+        throw Error('stateProofMessage must be a Uint8Array.');
+    } else {
+      txn.stateProofMessage = new Uint8Array(0);
+    }
+    if (txn.stateProof !== undefined) {
+      if (txn.stateProof.constructor !== Uint8Array)
+        throw Error('stateProof must be a Uint8Array.');
+    } else {
+      txn.stateProof = new Uint8Array(0);
+    }
   }
 
   // eslint-disable-next-line camelcase
@@ -826,6 +853,42 @@ export class Transaction implements TransactionStorageStructure {
       if (txn.grp === undefined) delete txn.grp;
       return txn;
     }
+    if (this.type === 'stpf') {
+      // state proof txn
+      const txn: EncodedTransaction = {
+        fee: this.fee,
+        fv: this.firstRound,
+        lv: this.lastRound,
+        note: Buffer.from(this.note),
+        snd: Buffer.from(this.from.publicKey),
+        type: this.type,
+        gen: this.genesisID,
+        gh: this.genesisHash,
+        lx: Buffer.from(this.lease),
+        sptype: this.stateProofType,
+        spmsg: Buffer.from(this.stateProofMessage),
+        sp: Buffer.from(this.stateProof),
+      };
+      // allowed zero values
+      if (!txn.sptype) delete txn.sptype;
+      if (!txn.note.length) delete txn.note;
+      if (!txn.lx.length) delete txn.lx;
+      if (!txn.amt) delete txn.amt;
+      if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
+      if (!txn.gen) delete txn.gen;
+      if (!txn.apid) delete txn.apid;
+      if (!txn.apaa || !txn.apaa.length) delete txn.apaa;
+      if (!txn.apap) delete txn.apap;
+      if (!txn.apsu) delete txn.apsu;
+      if (!txn.apan) delete txn.apan;
+      if (!txn.apfa || !txn.apfa.length) delete txn.apfa;
+      if (!txn.apas || !txn.apas.length) delete txn.apas;
+      if (!txn.apat || !txn.apat.length) delete txn.apat;
+      if (!txn.apep) delete txn.apep;
+      if (txn.grp === undefined) delete txn.grp;
+      return txn;
+    }
 
     return undefined;
   }
@@ -993,6 +1056,16 @@ export class Transaction implements TransactionStorageStructure {
       if (txnForEnc.apas !== undefined) {
         txn.appForeignAssets = txnForEnc.apas;
       }
+    } else if (txnForEnc.type === 'stpf') {
+      if (txnForEnc.sptype !== undefined) {
+        txn.stateProofType = txnForEnc.sptype;
+      }
+      if (txnForEnc.sp !== undefined) {
+        txn.stateProof = txnForEnc.sp;
+      }
+      if (txnForEnc.spmsg !== undefined) {
+        txn.stateProofMessage = txnForEnc.spmsg;
+      }
     }
     return txn;
   }
diff --git a/src/types/blockHeader.ts b/src/types/blockHeader.ts
index 7fa1c2e..96f2af2 100644
--- a/src/types/blockHeader.ts
+++ b/src/types/blockHeader.ts
@@ -65,7 +65,17 @@ export default interface BlockHeader {
   ts: number;
 
   /**
-   * Transaction root
+   * Transaction root SHA512_256
    */
   txn: string;
+
+  /**
+   * Transaction root SHA256
+   */
+  txn256: string;
+
+  /**
+   * StateProofTracking map of type to tracking data
+   */
+  spt: Map<number, Uint8Array>;
 }
diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts
index 739c88e..8c6db32 100644
--- a/src/types/transactions/base.ts
+++ b/src/types/transactions/base.ts
@@ -33,6 +33,10 @@ export enum TransactionType {
    * Application transaction
    */
   appl = 'appl',
+  /**
+   * State proof transaction
+   */
+  stpf = 'stpf',
 }
 
 export function isTransactionType(s: string): s is TransactionType {
@@ -42,7 +46,8 @@ export function isTransactionType(s: string): s is TransactionType {
     s === TransactionType.acfg ||
     s === TransactionType.axfer ||
     s === TransactionType.afrz ||
-    s === TransactionType.appl
+    s === TransactionType.appl ||
+    s === TransactionType.stpf
   );
 }
 
@@ -385,4 +390,19 @@ export interface TransactionParams {
    * Int representing extra pages of memory to rent during an application create transaction.
    */
   extraPages?: number;
+
+  /**
+   * Uint64 identifying a particular configuration of state proofs.
+   */
+  stateProofType?: number | bigint;
+
+  /**
+   * Byte array containing the state proof.
+   */
+  stateProof?: Uint8Array;
+
+  /**
+   * Byte array containing the state proof message.
+   */
+  stateProofMessage?: Uint8Array;
 }
diff --git a/src/types/transactions/encoded.ts b/src/types/transactions/encoded.ts
index bc5b9e9..1fe8fff 100644
--- a/src/types/transactions/encoded.ts
+++ b/src/types/transactions/encoded.ts
@@ -296,6 +296,21 @@ export interface EncodedTransaction {
    * extraPages
    */
   apep?: number;
+
+  /**
+   * stateProofType
+   */
+  sptype?: number | bigint;
+
+  /**
+   * stateProof
+   */
+  sp?: Buffer;
+
+  /**
+   * stateProofMessage
+   */
+  spmsg?: Buffer;
 }
 
 export interface EncodedSubsig {
diff --git a/src/types/transactions/index.ts b/src/types/transactions/index.ts
index 3202259..d73e46f 100644
--- a/src/types/transactions/index.ts
+++ b/src/types/transactions/index.ts
@@ -16,6 +16,7 @@ import {
   ApplicationClearStateTransaction as AppClearStateTxn,
   ApplicationNoOpTransaction as AppNoOpTxn,
 } from './application';
+import StateProofTxn from './stateproof';
 
 // Utilities
 export { TransactionParams, TransactionType, SuggestedParams } from './base';
@@ -44,6 +45,7 @@ export {
   ApplicationClearStateTransaction as AppClearStateTxn,
   ApplicationNoOpTransaction as AppNoOpTxn,
 } from './application';
+export { default as StateProofTxn } from './stateproof';
 
 // All possible transaction types
 type AnyTransaction =
@@ -60,5 +62,6 @@ type AnyTransaction =
   | AppOptInTxn
   | AppCloseOutTxn
   | AppClearStateTxn
-  | AppNoOpTxn;
+  | AppNoOpTxn
+  | StateProofTxn;
 export default AnyTransaction;
diff --git a/src/types/transactions/stateproof.ts b/src/types/transactions/stateproof.ts
new file mode 100644
index 0000000..7f2e116
--- /dev/null
+++ b/src/types/transactions/stateproof.ts
@@ -0,0 +1,17 @@
+import { TransactionType, TransactionParams } from './base';
+import { ConstructTransaction } from './builder';
+
+type SpecificParameters = Pick<
+  TransactionParams,
+  'stateProofType' | 'stateProof' | 'stateProofMessage'
+>;
+
+interface Overwrites {
+  type?: TransactionType.stpf;
+}
+
+type StateProofTransaction = ConstructTransaction<
+  SpecificParameters,
+  Overwrites
+>;
+export default StateProofTransaction;


Test patch:
diff --git a/tests/5.Transaction.js b/tests/5.Transaction.js
index 710d16e..eeaadc5 100644
--- a/tests/5.Transaction.js
+++ b/tests/5.Transaction.js
@@ -341,6 +341,40 @@ describe('Sign', () => {
       assert.deepStrictEqual(reencRep, encRep);
     });
 
+    it('should correctly serialize and deserialize a state proof transaction from msgpack representation', () => {
+      const o = {
+        from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
+        fee: 10,
+        firstRound: 51,
+        lastRound: 61,
+        note: new Uint8Array([123, 12, 200]),
+        genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+        voteKey: '5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKE=',
+        selectionKey: 'oImqaSLjuZj63/bNSAjd+eAh5JROOJ6j1cY4eGaJGX4=',
+        voteFirst: 123,
+        voteLast: 456,
+        voteKeyDilution: 1234,
+        genesisID: '',
+        type: 'stpf',
+        stateProofType: 0,
+        stateProof: new Uint8Array([1, 1, 1, 1]),
+        stateProofMessage: new Uint8Array([0, 0, 0, 0]),
+      };
+      const expectedTxn = new algosdk.Transaction(o);
+      console.log(
+        `${expectedTxn.stateProofType} ${expectedTxn.stateProofMessage} ${expectedTxn.stateProof} ${expectedTxn.type}`
+      );
+      const encRep = expectedTxn.get_obj_for_encoding();
+      console.log(
+        `${encRep.sptype} ${encRep.spmsg} ${encRep.sp} ${encRep.type}`
+      );
+      const encTxn = algosdk.encodeObj(encRep);
+      const decEncRep = algosdk.decodeObj(encTxn);
+      const decTxn = algosdk.Transaction.from_obj_for_encoding(decEncRep);
+      const reencRep = decTxn.get_obj_for_encoding();
+      assert.deepStrictEqual(reencRep, encRep);
+    });
+
     it('should correctly serialize and deserialize a key registration transaction from msgpack representation', () => {
       const o = {
         from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
diff --git a/tests/cucumber/steps/index.js b/tests/cucumber/steps/index.js
index 0c9c51f..0117dc8 100644
--- a/tests/cucumber/steps/index.js
+++ b/tests/cucumber/steps/index.js
@@ -347,6 +347,16 @@ if (browser) {
             rpcArgs = args.slice(0, rpcArgs.length - 1);
           }
 
+          for (const arg of rpcArgs) {
+            if (arg instanceof Uint8Array) {
+              // cannot send Uint8Array or Buffer objects because the arguments will get JSON
+              // encoded when transmitted to the browser
+              throw new Error(
+                `Attempted to send binary data to the browser when invoking test '${type} ${name}'`
+              );
+            }
+          }
+
           const { error } = await driver.executeAsyncScript(
             // variables are `scoped` because they exist in the upper scope
             async (scopedType, scopedName, ...rest) => {
@@ -391,34 +401,54 @@ for (const name of Object.keys(steps.given)) {
   const fn = steps.given[name];
   if (name === 'mock http responses in {string} loaded from {string}') {
     Given(name, function (fileName, jsonDirectory) {
-      const body1 = setupMockServerForResponses(
+      let body1 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         algodMockServerResponder
       );
-      const body2 = setupMockServerForResponses(
+      let body2 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         indexerMockServerResponder
       );
-      return fn.call(this, body2 || body1);
+      let format = 'json';
+      if (fileName.endsWith('base64')) {
+        format = 'msgp';
+      }
+      if (Buffer.isBuffer(body1)) {
+        body1 = body1.toString('base64');
+      }
+      if (Buffer.isBuffer(body2)) {
+        body2 = body2.toString('base64');
+      }
+      return fn.call(this, body2 || body1, format);
     });
   } else if (
     name ===
     'mock http responses in {string} loaded from {string} with status {int}.'
   ) {
     Given(name, function (fileName, jsonDirectory, status) {
-      const body1 = setupMockServerForResponses(
+      let body1 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         algodMockServerResponder
       );
-      const body2 = setupMockServerForResponses(
+      let body2 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         indexerMockServerResponder
       );
-      return fn.call(this, body2 || body1, status);
+      let format = 'json';
+      if (fileName.endsWith('base64')) {
+        format = 'msgp';
+      }
+      if (Buffer.isBuffer(body1)) {
+        body1 = body1.toString('base64');
+      }
+      if (Buffer.isBuffer(body2)) {
+        body2 = body2.toString('base64');
+      }
+      return fn.call(this, body2 || body1, status, format);
     });
   } else if (name === 'mock server recording request paths') {
     Given(name, function () {
diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js
index dabaa82..22f8ddc 100644
--- a/tests/cucumber/steps/steps.js
+++ b/tests/cucumber/steps/steps.js
@@ -1591,13 +1591,20 @@ module.exports = function getSteps(options) {
   } = options;
 
   let expectedMockResponse;
+  let responseFormat;
 
   Given(
     'mock http responses in {string} loaded from {string}',
-    function (expectedBody) {
+    function (expectedBody, format) {
       if (expectedBody !== null) {
         expectedMockResponse = expectedBody;
+        if (format === 'msgp') {
+          expectedMockResponse = new Uint8Array(
+            Buffer.from(expectedMockResponse, 'base64')
+          );
+        }
       }
+      responseFormat = format;
       this.v2Client = new algosdk.Algodv2(
         '',
         `http://${mockAlgodResponderHost}`,
@@ -1615,10 +1622,16 @@ module.exports = function getSteps(options) {
 
   Given(
     'mock http responses in {string} loaded from {string} with status {int}.',
-    function (expectedBody, status) {
+    function (expectedBody, status, format) {
       if (expectedBody !== null) {
         expectedMockResponse = expectedBody;
+        if (format === 'msgp') {
+          expectedMockResponse = new Uint8Array(
+            Buffer.from(expectedMockResponse, 'base64')
+          );
+        }
       }
+      responseFormat = format;
       this.v2Client = new algosdk.Algodv2(
         '',
         `http://${mockAlgodResponderHost}`,
@@ -1642,7 +1655,11 @@ module.exports = function getSteps(options) {
       try {
         if (client === 'algod') {
           // endpoints are ignored by mock server, see setupMockServerForResponses
-          this.actualMockResponse = await this.v2Client.status().do();
+          if (responseFormat === 'msgp') {
+            this.actualMockResponse = await this.v2Client.block(0).do();
+          } else {
+            this.actualMockResponse = await this.v2Client.status().do();
+          }
         } else if (client === 'indexer') {
           // endpoints are ignored by mock server, see setupMockServerForResponses
           this.actualMockResponse = await this.indexerClient
@@ -1668,10 +1685,22 @@ module.exports = function getSteps(options) {
 
   Then('the parsed response should equal the mock response.', function () {
     if (this.expectedMockResponseCode === 200) {
-      assert.strictEqual(
-        JSON.stringify(JSON.parse(expectedMockResponse)),
-        JSON.stringify(this.actualMockResponse)
-      );
+      // assert.deepStrictEqual considers a Buffer and Uint8Array with the same contents as unequal.
+      // These types are fairly interchangable in different parts of the SDK, so we need to normalize
+      // them before comparing, which is why we chain encoding/decoding below.
+      if (responseFormat === 'json') {
+        assert.strictEqual(
+          JSON.stringify(JSON.parse(expectedMockResponse)),
+          JSON.stringify(this.actualMockResponse)
+        );
+      } else {
+        assert.deepStrictEqual(
+          algosdk.decodeObj(
+            new Uint8Array(algosdk.encodeObj(this.actualMockResponse))
+          ),
+          algosdk.decodeObj(expectedMockResponse)
+        );
+      }
     }
   });
 
@@ -4454,6 +4483,17 @@ module.exports = function getSteps(options) {
     }
   );
 
+  When(
+    'we make a GetLightBlockHeaderProof call for round {int}',
+    async function (int) {
+      await this.v2Client.getLightBlockHeaderProof(int).do();
+    }
+  );
+
+  When('we make a GetStateProof call for round {int}', async function (int) {
+    await this.v2Client.getStateProof(int).do();
+  });
+
   Given(
     'a base64 encoded program bytes for heuristic sanity check {string}',
     async function (programByteStr) {
diff --git a/tests/cucumber/unit.tags b/tests/cucumber/unit.tags
index 4062ead..2d3f46b 100644
--- a/tests/cucumber/unit.tags
+++ b/tests/cucumber/unit.tags
@@ -17,7 +17,10 @@
 @unit.responses.231
 @unit.responses.unlimited_assets
 @unit.sourcemap
+@unit.stateproof.paths
+@unit.stateproof.responses
+@unit.stateproof.responses.msgp
 @unit.tealsign
 @unit.transactions
 @unit.transactions.keyreg
-@unit.transactions.payment
\ No newline at end of file
+@unit.transactions.payment