Skip to content

Commit

Permalink
Merge pull request #21 from algorandfoundation/feat-computeGroupId
Browse files Browse the repository at this point in the history
feat: Add computeGroupId support in Encoder
  • Loading branch information
ehanoc authored Feb 13, 2025
2 parents 602a357 + da3a1c6 commit 0f6f872
Show file tree
Hide file tree
Showing 6 changed files with 2,234 additions and 833 deletions.
203 changes: 200 additions & 3 deletions lib/algorand.encoder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { type SignKeyPair, randomBytes } from "tweetnacl"
import { sha512_256 } from "js-sha512"
import base32 from "hi-base32"
import { ALGORAND_ADDRESS_BAD_CHECKSUM_ERROR_MSG, AlgorandEncoder, MALFORMED_ADDRESS_ERROR_MSG } from "./algorand.encoder"
Expand All @@ -10,6 +9,10 @@ import {AssetParamsBuilder} from "./algorand.asset.params";
import {AssetConfigTransaction} from "./algorand.transaction.acfg";
import {AssetFreezeTransaction} from "./algorand.transaction.afrz";
import {AssetTransferTransaction} from "./algorand.transaction.axfer";
import algosdk from 'algosdk'
import { randomBytes } from "crypto"
import nacl from "tweetnacl"
import { SignedTransaction } from "./algorand.transaction"

export function concatArrays(...arrs: ArrayLike<number>[]) {
const size = arrs.reduce((sum, arr) => sum + arr.length, 0)
Expand Down Expand Up @@ -46,7 +49,7 @@ describe("Algorand Encoding", () => {
const signature: Uint8Array = new Uint8Array(Buffer.from(randomBytes(64)))
const signedTransaction: Uint8Array = algorandCrafter.addSignature(encodedTransaction, signature)

const decodedSignedTransaction: object = algoEncoder.decodeSignedTransaction(signedTransaction)
const decodedSignedTransaction: SignedTransaction = algoEncoder.decodeSignedTransaction(signedTransaction)
expect(decodedSignedTransaction).toBeDefined()
expect(decodedSignedTransaction).toEqual({
sig: signature,
Expand Down Expand Up @@ -228,7 +231,7 @@ describe("Algorand Encoding", () => {
expect(encoded).toEqual(algoEncoder.encodeTransaction(txn))
})
it("(OK) Encode & Decode Address ", async () => {
const keyPair: SignKeyPair = {
const keyPair = {
publicKey: Uint8Array.from([
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 20,
]),
Expand Down Expand Up @@ -275,4 +278,198 @@ describe("Algorand Encoding", () => {
algoEncoder.decodeAddress(address)
}).toThrowError(ALGORAND_ADDRESS_BAD_CHECKSUM_ERROR_MSG)
})

describe("Transaction Groups", () => {
it("(OK) Legacy AlgoSDK - Encoding of transaction group", async () => {
const keyPair = {
publicKey: Uint8Array.from([
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 20,
]),
secretKey: Uint8Array.from([
129, 128, 61, 158, 124, 215, 83, 137, 85, 47, 135, 151, 18, 162, 131, 63, 233, 138, 189, 56, 18, 114, 209, 4, 4, 128, 0, 159, 159, 76, 39, 85,
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 20,
]),
}

const sender: string = algoEncoder.encodeAddress(Buffer.from(keyPair.publicKey))

const expectedTxn = new algosdk.Transaction({
type: algosdk.TransactionType.pay,
sender,
paymentParams: {
receiver:
'UCE2U2JC4O4ZR6W763GUQCG57HQCDZEUJY4J5I6VYY4HQZUJDF7AKZO5GM',
amount: 847,
},
suggestedParams: {
minFee: 1000,
fee: 10,
firstValid: 51,
lastValid: 61,
genesisHash: algosdk.base64ToBytes(
'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI='
),
genesisID: 'mock-network',
},
note: new Uint8Array([123, 12, 200]),
});

expectedTxn.signTxn(keyPair.secretKey);

expectedTxn.group = algosdk.computeGroupID([expectedTxn]);
const encTxn = algosdk.encodeMsgpack(expectedTxn);
const decTxn = algosdk.decodeMsgpack(encTxn, algosdk.Transaction);
expect(decTxn).toEqual(expectedTxn);

const encRep = expectedTxn.toEncodingData();
const reencRep = decTxn.toEncodingData();
expect(reencRep).toEqual(encRep);
})

it("(OK) Encoding of transaction group", async () => {
const keyPair = {
publicKey: Uint8Array.from([
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 20,
]),
secretKey: Uint8Array.from([
129, 128, 61, 158, 124, 215, 83, 137, 85, 47, 135, 151, 18, 162, 131, 63, 233, 138, 189, 56, 18, 114, 209, 4, 4, 128, 0, 159, 159, 76, 39, 85,
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 20,
]),
}

const sender: string = algoEncoder.encodeAddress(Buffer.from(keyPair.publicKey))
const receiver: string = "UCE2U2JC4O4ZR6W763GUQCG57HQCDZEUJY4J5I6VYY4HQZUJDF7AKZO5GM"
const amount: number = 847
const firstValidRound: number = 51
const lastValidRound: number = 61
const genesisHashStr: string = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI="
const genesisID: string = "mock-network"
const fee: number = 100000

const crafter: AlgorandTransactionCrafter = new AlgorandTransactionCrafter(genesisID, genesisHashStr)

// Build pay transaction
const payTxn: PayTransaction = crafter.pay(amount, sender, receiver)
.addFirstValidRound(firstValidRound)
.addLastValidRound(lastValidRound)
.addFee(fee)
.addAmount(amount)
.get()

let modelsEncodedTx: Uint8Array = payTxn.encode()

const expectedTxn = new algosdk.Transaction({
type: algosdk.TransactionType.pay,
sender,
paymentParams: {
receiver,
amount,
},
suggestedParams: {
minFee: 100,
fee,
flatFee: true,
firstValid: firstValidRound,
lastValid: lastValidRound,
genesisHash: algosdk.base64ToBytes(
genesisHashStr
),
genesisID,
},
});

// algosdk sign
expectedTxn.signTxn(keyPair.secretKey);

// models sign
const sig: Uint8Array = nacl.sign.detached(modelsEncodedTx, keyPair.secretKey)

// attach sig
const signedTxModels: Uint8Array = crafter.addSignature(modelsEncodedTx, sig)

const bytesToSign: Uint8Array = expectedTxn.bytesToSign()
expect(modelsEncodedTx).toEqual(bytesToSign)

expectedTxn.group = algosdk.computeGroupID([expectedTxn]);

// Compute correct group ID with models when signature is present on txns
const modelsGroupId: Uint8Array = new AlgorandEncoder().computeGroupId([signedTxModels])
expect(expectedTxn.group).toEqual(modelsGroupId)

// Compute correct group ID with models when signature is NOT present on txns
const modelsGroupId2: Uint8Array = new AlgorandEncoder().computeGroupId([modelsEncodedTx])
expect(expectedTxn.group).toEqual(modelsGroupId2)
})

it("(OK) Encoding of transaction group with multiple transactions from different signers", async () => {
const keyPair1 = {
publicKey: Uint8Array.from([
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 20,
]),
secretKey: Uint8Array.from([
129, 128, 61, 158, 124, 215, 83, 137, 85, 47, 135, 151, 18, 162, 131, 63, 233, 138, 189, 56, 18, 114, 209, 4, 4, 128, 0, 159, 159, 76, 39, 85,
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 20,
]),
}

const keyPair2 = {
publicKey: Uint8Array.from([
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 21,
]),
secretKey: Uint8Array.from([
129, 128, 61, 158, 124, 215, 83, 137, 85, 47, 135, 151, 18, 162, 131, 63, 233, 138, 189, 56, 18, 114, 209, 4, 4, 128, 0, 159, 159, 76, 39, 85,
54, 40, 107, 229, 129, 45, 73, 38, 42, 70, 201, 214, 130, 182, 245, 154, 39, 250, 247, 34, 218, 97, 92, 98, 82, 0, 72, 242, 30, 197, 142, 21,
]),
}

const sender1: string = algoEncoder.encodeAddress(Buffer.from(keyPair1.publicKey))
const sender2: string = algoEncoder.encodeAddress(Buffer.from(keyPair2.publicKey))

const receiver: string = "UCE2U2JC4O4ZR6W763GUQCG57HQCDZEUJY4J5I6VYY4HQZUJDF7AKZO5GM"
const amount: number = 847
const firstValidRound: number = 51
const lastValidRound: number = 61
const genesisHashStr: string = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI="
const genesisID: string = "mock-network"
const fee: number = 100000

const crafter: AlgorandTransactionCrafter = new AlgorandTransactionCrafter(genesisID, genesisHashStr)

// Build pay transaction
const payTxn1: PayTransaction = crafter.pay(amount, sender1, receiver)
.addFirstValidRound(firstValidRound)
.addLastValidRound(lastValidRound)
.addFee(fee)
.addAmount(amount)
.get()

const payTxn2: PayTransaction = crafter.pay(amount, sender2, receiver)
.addFirstValidRound(firstValidRound)
.addLastValidRound(lastValidRound)
.addFee(fee)
.addAmount(amount)
.get()

// encode transactions
const encodedTxn1: Uint8Array = payTxn1.encode()
const encodedTxn2: Uint8Array = payTxn2.encode()

// sign transactions
const sig1: Uint8Array = nacl.sign.detached(encodedTxn1, keyPair1.secretKey)
const sig2: Uint8Array = nacl.sign.detached(encodedTxn2, keyPair2.secretKey)

// attach sigs
const signedTxn1: Uint8Array = crafter.addSignature(encodedTxn1, sig1)
const signedTxn2: Uint8Array = crafter.addSignature(encodedTxn2, sig2)

// group transactions
const group: Uint8Array = algoEncoder.computeGroupId([signedTxn1, signedTxn2])

// create expected group
const expectedGroup: Uint8Array = algoEncoder.computeGroupId([signedTxn1, signedTxn2])

// match group ids
expect(group).toEqual(expectedGroup)
})
})
})
46 changes: 38 additions & 8 deletions lib/algorand.encoder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { sha512_256 } from "js-sha512"
import * as msgpack from "algo-msgpack-with-bigint"
import base32 from "hi-base32"
import { PayTransaction } from "./algorand.transaction.pay.js"
import { Encoder } from "./encoder.role.js"
import { KeyregTransaction } from "./algorand.transaction.keyreg.js"
import { SignedTransaction, Transaction } from "./algorand.transaction.js"

const ALGORAND_PUBLIC_KEY_BYTE_LENGTH = 32
const ALGORAND_ADDRESS_BYTE_LENGTH = 36
Expand Down Expand Up @@ -74,7 +73,7 @@ export class AlgorandEncoder extends Encoder{
*
* @param tx
*/
encodeTransaction(tx: any): Uint8Array {
encodeTransaction(tx: Transaction): Uint8Array {
// [TAG] [AMT] .... [NOTE] [RCV] [SND] [] [TYPE]
const encoded: Uint8Array = msgpack.encode(tx, { sortKeys: true, ignoreUndefined: true })

Expand All @@ -92,23 +91,54 @@ export class AlgorandEncoder extends Encoder{
* @param encoded
* @returns
*/
decodeTransaction(encoded: Uint8Array): object | Error {
decodeTransaction(encoded: Uint8Array): Transaction {
const TAG: Buffer = Buffer.from("TX")
const tagBytes: number = TAG.byteLength

// remove tag Bytes for the tag and decode the rest
const decoded: object = msgpack.decode(encoded.slice(tagBytes)) as object
return decoded as PayTransaction | KeyregTransaction
return decoded as Transaction
}

/**
*
* @param encoded
* @returns
*/
decodeSignedTransaction(encoded: Uint8Array): object | Error {
const decoded: object = msgpack.decode(encoded) as object
return decoded as object
decodeSignedTransaction(encoded: Uint8Array): SignedTransaction {
const decoded: SignedTransaction = msgpack.decode(encoded) as SignedTransaction
return decoded
}

// calculate group id
computeGroupId(txns: Uint8Array[]): Uint8Array {
// ensure nr of txns in group are between 0 and 16
if (txns.length < 1 || txns.length > 16) throw new Error("Invalid number of transactions in group")


const hashes: Uint8Array[] = txns.map(txn => {
let encoded: Uint8Array

try {
// verify if it includes signature
const decodedTxn: SignedTransaction = this.decodeSignedTransaction(txn)
encoded = this.encodeTransaction(decodedTxn.txn)
} catch (error) {
// txn is already without signature, proceed without further processing
encoded = txn
}

return Uint8Array.from(sha512_256.array(encoded))
})

// encode { txList: [tx1, tx2, ...] } with msgpack
const encodedTxList: Uint8Array = msgpack.encode({ txlist: hashes }, { sortKeys: true, ignoreUndefined: true })

// Concat group tag + encoded
const concatTagList: Uint8Array = Encoder.ConcatArrays(Buffer.from("TG"), encodedTxList)

// return sha512_256 hash
return Uint8Array.from(sha512_256.array(concatTagList))
}

/**
Expand Down
13 changes: 13 additions & 0 deletions lib/algorand.transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ import {KeyregTransaction} from "./algorand.transaction.keyreg.js";
* @category Common
*/
export type Transaction = PayTransaction | AssetConfigTransaction | AssetTransferTransaction | AssetFreezeTransaction | KeyregTransaction

// SignedTransaction
export interface SignedTransaction {
/**
* Transaction
*/
txn: Transaction

/**
* Transaction Signature
*/
sig: Uint8Array
}
Loading

0 comments on commit 0f6f872

Please sign in to comment.