Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ede747b
feat: update javadocs and some checks for new v42 consensus limits (n…
Argimirodelpozo Aug 18, 2026
6e480ba
test: test large txns
Argimirodelpozo Aug 18, 2026
2715457
feat: support global state schema and extra page changes through upda…
Argimirodelpozo Aug 18, 2026
712c16c
test: new app update tests
Argimirodelpozo Aug 18, 2026
c5220f1
chore: dryrun removal
Argimirodelpozo Aug 19, 2026
19ea358
chore: update autogenerated files using algorand/generator
Argimirodelpozo Aug 19, 2026
16a36ba
avoid authAddr is sender
Argimirodelpozo Aug 19, 2026
9ebd6a2
feat: allow 0 in access-list app refs and treat the zero address as s…
Argimirodelpozo Aug 19, 2026
fd7254c
test: add access list tests excercising this
Argimirodelpozo Aug 19, 2026
8d2e08d
added heartbeat challenge discount field
Argimirodelpozo Aug 19, 2026
ed9d151
test: add test for heartbeat challenge discount field
Argimirodelpozo Aug 19, 2026
f9943f3
add empty refs as box I/O quota bump
Argimirodelpozo Aug 19, 2026
1baabcf
test: add tests for new resource ref and access list behavior
Argimirodelpozo Aug 19, 2026
0449e83
address @mrcointreau review findings: the inner holdingRef/LocalsRef/…
Argimirodelpozo Aug 19, 2026
6f289fa
@mrcointreau review: fix old bug that would assign local schema when …
Argimirodelpozo Aug 19, 2026
4ca5392
@mrcointreau review: dedup standalone access list entries
Argimirodelpozo Aug 19, 2026
cb1182a
@mrcointreau review: change resource collection order to match the ot…
Argimirodelpozo Aug 19, 2026
fde27af
@mrcointreau review: txn equality missing fields
Argimirodelpozo Aug 20, 2026
e7954cd
refactor: use authAddr setter so equality with sender implies empty
Argimirodelpozo Aug 20, 2026
0c80975
@mrcointreau review: some doc changes and cleanup
Argimirodelpozo Aug 20, 2026
df65ab2
test: some test improvements
Argimirodelpozo Aug 20, 2026
7709155
fix: empty references for 0 app, asset and account resource refs
Argimirodelpozo Aug 20, 2026
ecb76d3
feat: add PQ signature support
Argimirodelpozo Aug 21, 2026
41e94df
feat: add ed25519 callback based signers
Argimirodelpozo Aug 21, 2026
ebd79d0
Update src/main/java/com/algorand/algosdk/signer/Ed25519AlgorandSigne…
Argimirodelpozo Aug 22, 2026
48da3ee
Update src/main/java/com/algorand/algosdk/signer/Ed25519MultisigAlgor…
Argimirodelpozo Aug 22, 2026
3ac006a
Update src/test/java/com/algorand/algosdk/crypto/TestPQ.java
Argimirodelpozo Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ test-harness/
src/test/resources/generated_responses/
*.feature
src/test/resources/**/*.json
# vendored post-quantum test vectors
!src/test/resources/pq_test_data/*.json
src/test/resources/**/*.base64
src/test/resources/**/*.tok.map
src/test/resources/**/*.teal
Expand All @@ -29,6 +31,8 @@ target/

# Package Files #
*.jar
# vendored, not-yet-published falcon-det1024 test dependency (see test-lib/README.md)
!test-lib/**/*.jar
*.war
*.nar
*.ear
Expand Down
27 changes: 27 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,36 @@
<scope>test</scope>
</dependency>

<!-- Real Falcon-1024 (det1024) implementation, used only to back the
signing callback in the post-quantum cucumber steps. The SDK itself
bundles no Falcon code.

TEMPORARY: falcon-det1024-java is not published yet, so a build of
it is vendored under test-lib/ (see test-lib/README.md) and
resolved from the file-based repository declared below. When the
artifact reaches Maven Central, delete test-lib/ and the
<repositories> block and bump this to the released version. -->
<dependency>
<groupId>com.algorand</groupId>
<artifactId>falcon-det1024</artifactId>
<version>0.1.1</version>
<scope>test</scope>
</dependency>

<!-- end test dependencies -->
</dependencies>

<!-- TEMPORARY: resolves the vendored, not-yet-published falcon-det1024
test dependency. Remove once it is available from Maven Central. -->
<repositories>
<repository>
<id>vendored-test-lib</id>
<url>file://${project.basedir}/test-lib</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
Comment on lines +227 to +234

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test-lib/README.md already covers removing this once falcon-det1024 hits Maven Central, but there's no guard for the opposite ordering: if an SDK release goes out first, this block ships in the (immutable) published pom, since both deploy paths publish the raw pom and there's no flatten plugin. If a release could plausibly land before the falcon publish, cheap insurance is moving the block inside a profile or resolving the jar via an install-file step; otherwise fine as-is.


<build>
<plugins>
<!-- Newer version of surefire plugin for running JUnit5 tests. -->
Expand Down
25 changes: 16 additions & 9 deletions src/main/java/com/algorand/algosdk/account/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,8 @@ public SignedTransaction signTransaction(Transaction tx) throws NoSuchAlgorithmE
byte[] prefixEncodedTx = tx.bytesToSign();
Signature txSig = rawSignBytes(Arrays.copyOf(prefixEncodedTx, prefixEncodedTx.length));
SignedTransaction stx = new SignedTransaction(tx, txSig, tx.txID());
if (!tx.sender.equals(this.address)) {
stx.authAddr(this.address);
}
// the setter normalizes authAddr == sender to empty ("not rekeyed")
stx.authAddr(this.address);
return stx;
} catch (IOException e) {
throw new RuntimeException("unexpected behavior", e);
Expand Down Expand Up @@ -331,10 +330,9 @@ public SignedTransaction signMultisigTransaction(MultisigAddress from, Transacti
}
// generate signed transaction
SignedTransaction stx = new SignedTransaction(tx, mSig, txSig.transactionID);
// if the transaction sender address is not multi-sig address
// set the auth address as the multi-sig address
if (!tx.sender.equals(from.toAddress()))
stx.authAddr = from.toAddress();
// if the transaction sender is not the multi-sig address, the multi-sig
// address becomes the auth address (the setter normalizes sender to empty)
stx.authAddr(from.toAddress());
return stx;
}

Expand Down Expand Up @@ -451,6 +449,9 @@ public byte[] signMultisigTransactionBytes(MultisigAddress from, Transaction tx)
* @throws IOException
*/
public LogicsigSignature signLogicsig(LogicsigSignature lsig) throws IOException {
if (lsig.pqsig != null) {
throw new IllegalStateException("LogicsigSignature already has a post-quantum signature");
}
Signature sig;
try {
byte[] bytesToSign = lsig.bytesToSign();
Expand All @@ -470,6 +471,9 @@ public LogicsigSignature signLogicsig(LogicsigSignature lsig) throws IOException
* @throws IOException
*/
public LogicsigSignature signLogicsig(LogicsigSignature lsig, MultisigAddress ma) throws IOException {
if (lsig.pqsig != null) {
throw new IllegalStateException("LogicsigSignature already has a post-quantum signature");
}
Ed25519PublicKey myPK = this.getEd25519PublicKey();
int myIndex = ma.publicKeys.indexOf(myPK);
if (myIndex == -1) {
Expand Down Expand Up @@ -543,8 +547,8 @@ public static SignedTransaction signLogicTransactionWithAddress(LogicsigSignatur

try {
SignedTransaction stx = new SignedTransaction(tx, lsig, tx.txID());
if (!stx.tx.sender.equals(lsigAddr))
stx.authAddr = lsigAddr;
// the setter normalizes authAddr == sender to empty ("not rekeyed")
stx.authAddr(lsigAddr);
return stx;
} catch (Exception ex) {
throw new IOException("could not encode transactions", ex);
Expand All @@ -562,6 +566,7 @@ public static SignedTransaction signLogicsigTransaction(LogicsigSignature lsig,
boolean hasSig = lsig.sig != null;
boolean hasLmsig = lsig.lmsig != null;
boolean hasMsig = lsig.msig != null;
boolean hasPQsig = lsig.pqsig != null;
Address lsigAddr;
try {
if (hasSig) {
Expand All @@ -570,6 +575,8 @@ public static SignedTransaction signLogicsigTransaction(LogicsigSignature lsig,
lsigAddr = lsig.lmsig.convertToMultisigAddress().toAddress();
} else if (hasMsig) {
lsigAddr = lsig.msig.convertToMultisigAddress().toAddress();
} else if (hasPQsig) {
lsigAddr = PQAddress.fromSignature(lsig.pqsig);
} else {
lsigAddr = lsig.toAddress();
}
Expand Down
50 changes: 46 additions & 4 deletions src/main/java/com/algorand/algosdk/account/LogicSigAccount.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.algorand.algosdk.account;

import com.algorand.algosdk.crypto.*;
import com.algorand.algosdk.signer.Falcon1024AlgorandSigner;
import com.algorand.algosdk.signer.PQAlgorandSigner;
import com.algorand.algosdk.transaction.SignedTransaction;
import com.algorand.algosdk.transaction.Transaction;
import com.algorand.algosdk.transaction.TxnSigner;
Expand Down Expand Up @@ -82,6 +84,39 @@ public void appendMultiSig(PrivateKey privateKey)
signerAccount.appendToLogicsig(this.lsig);
}

/**
* Creates a new delegated LogicSigAccount whose delegating account is a
* post-quantum account, signing the program in one call with the supplied
* callback-based signer.
* <p>
* This is the callback-based counterpart of the
* {@code (logic, args, privateKey)} constructor. Post-quantum delegation
* does not support multisig; there is no multisig variant of this factory.
* @param logic the bytes of the program
* @param args the arguments of the program (may be null)
* @param signer the post-quantum signer of the delegating account
* @return a delegated LogicSigAccount carrying the post-quantum signature
*/
public static LogicSigAccount delegatedPQ(byte[] logic, List<byte[]> args, PQAlgorandSigner signer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PQ got one-call factories (delegatedPQ/delegatedFalcon1024) but there's no ed25519-callback equivalent, while js/py cover both with signWithSigner/sign_with_signer. The two-step composition works (signLogicsig + the verifying ctor), so just a nit: consider a matching delegated(logic, args, Ed25519AlgorandSigner) factory

throws Exception {
Objects.requireNonNull(signer, "signer must not be null");
LogicsigSignature lsig = new LogicsigSignature(logic, args);
signer.signLogicsig(lsig);
return new LogicSigAccount(lsig, null);
}

/**
* {@link #delegatedPQ} fixed to a Falcon-1024 signer.
* @param logic the bytes of the program
* @param args the arguments of the program (may be null)
* @param signer the Falcon-1024 signer of the delegating account
* @return a delegated LogicSigAccount carrying the post-quantum signature
*/
public static LogicSigAccount delegatedFalcon1024(byte[] logic, List<byte[]> args, Falcon1024AlgorandSigner signer)
throws Exception {
return delegatedPQ(logic, args, signer);
}

/**
* Creates a new delegated LogicSigAccount from existing LogicSig
* @param lsig is an existing LogicSig.
Expand All @@ -99,7 +134,7 @@ public LogicSigAccount(LogicsigSignature lsig, Ed25519PublicKey signerPublicKey)
boolean hasMsig = lsig.msig != null;

if (lsig.sigCount() > 1)
throw new IllegalArgumentException("Logicsig has too many signatures, at most one of Sig, Msig, or LMsig may be defined");
throw new IllegalArgumentException("Logicsig has too many signatures, at most one of Sig, Msig, LMsig, or PQsig may be defined");
if (hasSig) {
if (signerPublicKey == null)
throw new IllegalArgumentException("Cannot generate LogicSigAccount from single-signed LogicSig and a null public key");
Expand All @@ -110,7 +145,7 @@ public LogicSigAccount(LogicsigSignature lsig, Ed25519PublicKey signerPublicKey)
return;
}
if (signerPublicKey != null)
throw new IllegalArgumentException("Cannot generate LogicSigAccount from multi-sig LogicSig and a public key");
throw new IllegalArgumentException("Cannot generate LogicSigAccount from multi-sig or post-quantum LogicSig and a public key");
this.lsig = lsig;
this.sigKey = null;
}
Expand All @@ -123,7 +158,8 @@ public boolean isDelegated() {
boolean hasSig = this.lsig.sig != null;
boolean hasLmsig = this.lsig.lmsig != null;
boolean hasMsig = this.lsig.msig != null;
return hasSig || hasLmsig || hasMsig;
boolean hasPQsig = this.lsig.pqsig != null;
return hasSig || hasLmsig || hasMsig || hasPQsig;
}

/**
Expand All @@ -136,13 +172,19 @@ public Address getAddress() throws NoSuchAlgorithmException, IllegalArgumentExce
boolean hasSig = this.lsig.sig != null;
boolean hasLmsig = this.lsig.lmsig != null;
boolean hasMsig = this.lsig.msig != null;
boolean hasPQsig = this.lsig.pqsig != null;

if (this.lsig.sigCount() > 1)
throw new IllegalArgumentException("Logicsig has too many signatures, at most one of Sig, Msig, or LMsig may be defined");
throw new IllegalArgumentException("Logicsig has too many signatures, at most one of Sig, Msig, LMsig, or PQsig may be defined");
if (hasSig) {
byte[] sigKeyRaw = this.sigKey.getBytes();
return new Address(sigKeyRaw);
}
if (hasPQsig) {
// The signature carries the scheme, salt and public key of the
// delegating account, so it fully determines the address.
return PQAddress.fromSignature(this.lsig.pqsig);
}
if (hasLmsig) {
List<Ed25519PublicKey> pkFromSubSig = new ArrayList<>();
for (MultisigSignature.MultisigSubsig subSig : this.lsig.lmsig.subsigs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public HoldingReference(Address address, long assetId) {

/**
* Represents a locals reference for local state of an account in an app.
* An appId of 0 refers to the currently executing app.
*/
public static class LocalsReference {
public final Address address;
Expand All @@ -47,6 +48,7 @@ public LocalsReference(Address address, long appId) {
private List<AppBoxReference> appBoxReferences;
private List<HoldingReference> holdings;
private List<LocalsReference> locals;
private int emptyRefs = 0;
private Long applicationId;
private Long rejectVersion;
private boolean useAccess = false;
Expand All @@ -66,7 +68,8 @@ protected void applyTo(Transaction txn) {

// Check if advanced features are being used
boolean hasAdvancedFeatures = (holdings != null && !holdings.isEmpty()) ||
(locals != null && !locals.isEmpty());
(locals != null && !locals.isEmpty()) ||
emptyRefs > 0;

if (useAccess) {
// Using access field mode - translate all references into access list
Expand All @@ -78,21 +81,18 @@ protected void applyTo(Transaction txn) {
}
}

if (foreignApps != null && !foreignApps.isEmpty()) {
for (Long appId : foreignApps) {
allRefs.add(AppResourceRef.forApp(appId));
}
}

// Collection order is a cross-SDK contract (py/js): accounts,
// assets, apps, holdings, locals, boxes. Reordering changes the
// encoded bytes and therefore TxIDs and group IDs.
if (foreignAssets != null && !foreignAssets.isEmpty()) {
for (Long assetId : foreignAssets) {
allRefs.add(AppResourceRef.forAsset(assetId));
}
}

if (appBoxReferences != null && !appBoxReferences.isEmpty()) {
for (AppBoxReference boxRef : appBoxReferences) {
allRefs.add(AppResourceRef.forBox(boxRef.getAppId(), boxRef.getName()));
if (foreignApps != null && !foreignApps.isEmpty()) {
for (Long appId : foreignApps) {
allRefs.add(AppResourceRef.forApp(appId));
}
}

Expand All @@ -108,13 +108,23 @@ protected void applyTo(Transaction txn) {
}
}

if (appBoxReferences != null && !appBoxReferences.isEmpty()) {
for (AppBoxReference boxRef : appBoxReferences) {
allRefs.add(AppResourceRef.forBox(boxRef.getAppId(), boxRef.getName()));
}
}

for (int i = 0; i < emptyRefs; i++) {
allRefs.add(AppResourceRef.forEmpty());
}

txn.access = AccessConverter.convertToResourceRefs(allRefs, sender, applicationId);

} else {
// Using legacy fields mode
if (hasAdvancedFeatures) {
throw new IllegalArgumentException(
"Holdings and locals references require useAccess=true as they cannot be represented in legacy transaction format"
"Holdings, locals, and empty references require useAccess=true as they cannot be represented in legacy transaction format"
);
}

Expand Down Expand Up @@ -203,6 +213,8 @@ public T boxReferences(List<AppBoxReference> boxReferences) {
* Set asset holding references that need to be accessible in this transaction.
* Holdings references allow the transaction to access asset balances of specific accounts.
*
* A null or zero (empty) address means the sender.
*
* Note: Holdings references are only available when useAccess=true as they cannot be
* represented in legacy transaction format.
*/
Expand All @@ -215,6 +227,8 @@ public T holdings(List<HoldingReference> holdings) {
* Set local state references that need to be accessible in this transaction.
* Locals references allow the transaction to access local state of specific accounts in specific apps.
*
* A null or zero (empty) address means the sender; an appId of 0 refers to the currently executing app.
*
* Note: Locals references are only available when useAccess=true as they cannot be
* represented in legacy transaction format.
*/
Expand All @@ -223,6 +237,20 @@ public T locals(List<LocalsReference> locals) {
return (T) this;
}

/**
* Add empty references to the access list. Each empty reference requests a
* box I/O quota bump without naming a resource.
*
* Note: Empty references are only available when useAccess=true.
*/
public T emptyRefs(int emptyRefs) {
if (emptyRefs < 0) {
throw new IllegalArgumentException("emptyRefs must be a non-negative integer");
}
this.emptyRefs = emptyRefs;
return (T) this;
}

/**
* Enable or disable translation of foreign references into the access field.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public interface ApplicationCallReferencesSetter<T extends ApplicationCallRefere

/**
* Accounts lists the accounts (in addition to the sender) that may be accessed from the application logic.
* In access list mode (useAccess=true) the zero (empty) address means the sender and is never listed.
*/
public T accounts(List<Address> accounts);

Expand Down
Loading
Loading