From 7f1e36f4485e6a0374269c4968c0e848300a69d0 Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Thu, 18 Jun 2026 12:02:25 +0800 Subject: [PATCH 1/3] refactor!: return signature SCVal from Auth.Signer for custom account contracts --- CHANGELOG.md | 26 ++ src/main/java/org/stellar/sdk/Auth.java | 199 +++++++----- src/main/java/org/stellar/sdk/Util.java | 21 ++ .../org/stellar/sdk/scval/ScvComparator.java | 29 +- src/test/java/org/stellar/sdk/AuthTest.java | 301 ++++++++++++------ src/test/kotlin/org/stellar/sdk/UtilTest.kt | 28 ++ 6 files changed, 407 insertions(+), 197 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d0aa8bd..6d6d73467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ ## Pending +### Breaking changes +- refactor: redesign `Auth.Signer` to natively support custom account contracts (BLS, WebAuthn / secp256r1, threshold, policy contracts, ...). + - `Auth.Signer.sign` now returns the signature `SCVal` accepted by the account contract at the credential node being signed — the default Stellar Account shape for a `G...` account, or whatever a custom account contract's `__check_auth` expects — instead of an `Auth.Signature`. The returned value is attached verbatim. This also unlocks signing a custom-contract delegate node of a `SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES` entry (CAP-71-01). + - `Auth.Signature` has been removed. + - Client-side ed25519 signature verification inside `authorizeEntry` is removed; the network performs the authoritative check via the account contract's `__check_auth` (a `null` return from a `Signer` is still rejected). + - `Auth.authorizeInvocation(Signer, ...)`: the `publicKey` parameter is renamed to `address` (it already accepted `C...` contract addresses). + - New `Auth.authorizationPayloadHash(HashIDPreimage)` returns the 32-byte payload an account contract receives in `__check_auth`. + - New `Auth.defaultAccountSignatureScVal` builds the default Stellar Account signature shape (`Vec`), the building block for ed25519 `Signer`s that sign via an HSM or remote service. Two overloads accept either the raw `byte[]` ed25519 public key or a `G...` `accountId`. The `KeyPair` overloads are unchanged and produce byte-identical output. + + Migration for a custom (non-`KeyPair`) `Signer` targeting a classic `G...` account: + + ```java + // before + Auth.Signer signer = + preimage -> { + byte[] payload = Util.hash(preimage.toXdrByteArray()); + return new Auth.Signature(accountId, signRemotely(payload)); + }; + + // after + Auth.Signer signer = + preimage -> + Auth.defaultAccountSignatureScVal( + accountId, signRemotely(Auth.authorizationPayloadHash(preimage))); + ``` + ## 4.0.0-beta0 ### Update diff --git a/src/main/java/org/stellar/sdk/Auth.java b/src/main/java/org/stellar/sdk/Auth.java index faf14ab5a..743738117 100644 --- a/src/main/java/org/stellar/sdk/Auth.java +++ b/src/main/java/org/stellar/sdk/Auth.java @@ -161,17 +161,9 @@ public static SorobanAuthorizationEntry authorizeEntry( Network network, @Nullable String forAddress) { Signer entrySigner = - preimage -> { - byte[] data; - try { - data = preimage.toXdrByteArray(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to convert preimage to bytes", e); - } - byte[] payload = Util.hash(data); - byte[] signature = signer.sign(payload); - return new Signature(signer.getAccountId(), signature); - }; + preimage -> + defaultAccountSignatureScVal( + signer.getPublicKey(), signer.sign(authorizationPayloadHash(preimage))); return authorizeEntry(entry, entrySigner, validUntilLedgerSeq, network, forAddress); } @@ -192,8 +184,8 @@ public static SorobanAuthorizationEntry authorizeEntry( * * * @param entry a base64 encoded unsigned Soroban authorization entry - * @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the - * signature of the hash of the raw payload bytes, see {@link Signer} + * @param signer a {@link Signer} that takes the authorization preimage (a {@link HashIDPreimage}) + * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param network the network is incorprated into the signature @@ -220,8 +212,8 @@ public static SorobanAuthorizationEntry authorizeEntry( * * * @param entry a base64 encoded unsigned Soroban authorization entry - * @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the - * signature of the hash of the raw payload bytes, see {@link Signer} + * @param signer a {@link Signer} that takes the authorization preimage (a {@link HashIDPreimage}) + * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param network the network is incorprated into the signature @@ -260,8 +252,8 @@ public static SorobanAuthorizationEntry authorizeEntry( * * * @param entry an unsigned Soroban authorization entry - * @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the - * signature of the hash of the raw payload bytes, see {@link Signer} + * @param signer a {@link Signer} that takes the authorization preimage (a {@link HashIDPreimage}) + * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param network the network is incorprated into the signature @@ -294,8 +286,8 @@ public static SorobanAuthorizationEntry authorizeEntry( * credentials are returned unchanged (they are covered by the transaction envelope signature). * * @param entry an unsigned Soroban authorization entry - * @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the - * signature of the hash of the raw payload bytes, see {@link Signer} + * @param signer a {@link Signer} that takes the authorization preimage (a {@link HashIDPreimage}) + * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param network the network is incorprated into the signature @@ -344,33 +336,15 @@ public static SorobanAuthorizationEntry authorizeEntry( HashIDPreimage preimage = buildAuthorizationEntryPreimage(clone, validUntilLedgerSeq, network); - Signature signature = signer.sign(preimage); - - byte[] data; - try { - data = preimage.toXdrByteArray(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to convert preimage to bytes", e); - } - byte[] payload = Util.hash(data); - KeyPair kp = KeyPair.fromAccountId(signature.publicKey); - if (!kp.verify(payload, signature.signature)) { - throw new IllegalArgumentException("signature does not match payload"); + // The signer returns the final signature SCVal accepted by the account contract at this + // credential node — the default Stellar Account shape for a G... account, or whatever a custom + // account contract's __check_auth expects. It is attached verbatim: the network performs the + // authoritative check via __check_auth, so there is nothing to verify client-side. + SCVal signatureScVal = signer.sign(preimage); + if (signatureScVal == null) { + throw new IllegalArgumentException("signer returned a null signature"); } - // This structure is defined here: - // https://developers.stellar.org/docs/learn/encyclopedia/contract-development/contract-interactions/stellar-transaction#stellar-account-signatures - // https://github.com/stellar/rs-soroban-env/blob/99d8c92cdc7e5cd0f5311df8f88d04658ecde7d2/soroban-env-host/src/native_contract/account_contract.rs#L51 - SCVal sigScVal = - Scv.toMap( - new LinkedHashMap() { - { - put(Scv.toSymbol("public_key"), Scv.toBytes(kp.getPublicKey())); - put(Scv.toSymbol("signature"), Scv.toBytes(signature.getSignature())); - } - }); - SCVal signatureScVal = Scv.toVec(Collections.singleton(sigScVal)); - // CAP-71-01: the signature payload is shared across the top-level address and every // (possibly nested) delegate, so this signer's signature is written to whichever credential // node(s) carry `forAddress`. When no `forAddress` is given the signature goes to the @@ -463,15 +437,9 @@ public static SorobanAuthorizationEntry authorizeInvocation( Network network, SorobanCredentialsType credentialsType) { Signer entrySigner = - preimage -> { - try { - byte[] payload = Util.hash(preimage.toXdrByteArray()); - byte[] signature = signer.sign(payload); - return new Signature(signer.getAccountId(), signature); - } catch (IOException e) { - throw new UnexpectedException(e); - } - }; + preimage -> + defaultAccountSignatureScVal( + signer.getPublicKey(), signer.sign(authorizationPayloadHash(preimage))); return authorizeInvocation( entrySigner, signer.getAccountId(), @@ -501,9 +469,10 @@ public static SorobanAuthorizationEntry authorizeInvocation( * Auth#authorizeInvocation(Signer, String, Long, SorobanAuthorizedInvocation, Network, * SorobanCredentialsType)}. The default will flip to V2 once protocol 28 makes it mandatory. * - * @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the - * signature of the hash of the raw payload bytes, see {@link Signer} - * @param publicKey the public identity of the signer + * @param signer a {@link Signer} that takes the authorization preimage (a {@link HashIDPreimage}) + * and returns the signature {@link SCVal} the account at the entry's address expects + * @param address the address being authorized — a classic {@code G...} account or a {@code C...} + * contract address (the typical custom-account case) * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param invocation invocation the invocation tree that we're authorizing (likely, this comes @@ -513,13 +482,13 @@ public static SorobanAuthorizationEntry authorizeInvocation( */ public static SorobanAuthorizationEntry authorizeInvocation( Signer signer, - String publicKey, + String address, Long validUntilLedgerSeq, SorobanAuthorizedInvocation invocation, Network network) { return authorizeInvocation( signer, - publicKey, + address, validUntilLedgerSeq, invocation, network, @@ -540,9 +509,10 @@ public static SorobanAuthorizationEntry authorizeInvocation( *

This is in contrast to {@link Auth#authorizeEntry}, which signs an existing entry "in * place". * - * @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the - * signature of the hash of the raw payload bytes, see {@link Signer} - * @param publicKey the public identity of the signer + * @param signer a {@link Signer} that takes the authorization preimage (a {@link HashIDPreimage}) + * and returns the signature {@link SCVal} the account at the entry's address expects + * @param address the address being authorized — a classic {@code G...} account or a {@code C...} + * contract address (the typical custom-account case) * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param invocation invocation the invocation tree that we're authorizing (likely, this comes @@ -557,7 +527,7 @@ public static SorobanAuthorizationEntry authorizeInvocation( */ public static SorobanAuthorizationEntry authorizeInvocation( Signer signer, - String publicKey, + String address, Long validUntilLedgerSeq, SorobanAuthorizedInvocation invocation, Network network, @@ -565,7 +535,7 @@ public static SorobanAuthorizationEntry authorizeInvocation( long nonce = new SecureRandom().nextLong(); SorobanAddressCredentials addressCredentials = SorobanAddressCredentials.builder() - .address(new Address(publicKey).toSCAddress()) + .address(new Address(address).toSCAddress()) .nonce(new Int64(nonce)) .signatureExpirationLedger(new Uint32(new XdrUnsignedInteger(validUntilLedgerSeq))) .signature(Scv.toVoid()) @@ -622,6 +592,70 @@ public static SorobanAddressCredentials getAddressCredentials(SorobanCredentials } } + /** + * Returns the 32-byte payload that account contracts receive in {@code __check_auth}: the SHA-256 + * hash of the authorization preimage's XDR bytes. + * + *

Use this inside a custom {@link Signer} to obtain the exact bytes the host asks the account + * contract to verify, then return whatever signature {@link SCVal} the contract expects. It is + * the same payload the {@link KeyPair} signing path signs. + * + * @param preimage the Soroban authorization preimage, see {@link + * Auth#buildAuthorizationEntryPreimage(SorobanAuthorizationEntry, long, Network)} + * @return the SHA-256 hash of the preimage XDR bytes + */ + public static byte[] authorizationPayloadHash(HashIDPreimage preimage) { + try { + return Util.hash(preimage.toXdrByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to convert preimage to bytes", e); + } + } + + /** + * Builds the signature {@link SCVal} shape expected by the default Stellar Account contract: + * {@code Vec}. + * + *

This is the building block for ed25519 {@link Signer} implementations that sign elsewhere (a + * hardware module or remote signing service) yet target a classic {@code G...} account — the same + * shape the {@link KeyPair} signing path produces. Use this {@code byte[]} overload when you + * already hold the raw public key (e.g. from an HSM); use {@link + * Auth#defaultAccountSignatureScVal(String, byte[])} to pass a {@code G...} address instead. + * + * @param publicKey the raw 32-byte ed25519 public key that produced {@code signature} + * @param signature the 64-byte ed25519 signature over {@link + * Auth#authorizationPayloadHash(HashIDPreimage)} + * @return the default Stellar Account signature value + * @see Stellar Account signatures + */ + public static SCVal defaultAccountSignatureScVal(byte[] publicKey, byte[] signature) { + SCVal signatureStruct = + Scv.toMap( + new LinkedHashMap() { + { + put(Scv.toSymbol("public_key"), Scv.toBytes(publicKey)); + put(Scv.toSymbol("signature"), Scv.toBytes(signature)); + } + }); + return Scv.toVec(Collections.singleton(signatureStruct)); + } + + /** + * Builds the default Stellar Account signature {@link SCVal} from a classic {@code G...} account + * address — a convenience wrapper over {@link Auth#defaultAccountSignatureScVal(byte[], byte[])} + * that decodes {@code accountId} to its raw ed25519 public key. + * + * @param accountId the {@code G...} account whose ed25519 public key produced {@code signature} + * @param signature the 64-byte ed25519 signature over {@link + * Auth#authorizationPayloadHash(HashIDPreimage)} + * @return the default Stellar Account signature value + */ + public static SCVal defaultAccountSignatureScVal(String accountId, byte[] signature) { + return defaultAccountSignatureScVal(KeyPair.fromAccountId(accountId).getPublicKey(), signature); + } + /** * Builds the {@link HashIDPreimage} whose hash a signer must sign to authorize {@code entry}. * This is the low-level signature payload used by {@link Auth#authorizeEntry}, exposed for @@ -787,11 +821,13 @@ private static SorobanDelegateSignature[] buildDelegateNodes( new AbstractMap.SimpleImmutableEntry<>(scAddressXdrBytes(node.getAddress()), node)); } - keyedNodes.sort((a, b) -> compareBytes(a.getKey(), b.getKey())); + keyedNodes.sort((a, b) -> Util.compareBytesUnsigned(a.getKey(), b.getKey())); SorobanDelegateSignature[] nodes = new SorobanDelegateSignature[keyedNodes.size()]; for (int i = 0; i < keyedNodes.size(); i++) { - if (i > 0 && compareBytes(keyedNodes.get(i - 1).getKey(), keyedNodes.get(i).getKey()) == 0) { + if (i > 0 + && Util.compareBytesUnsigned(keyedNodes.get(i - 1).getKey(), keyedNodes.get(i).getKey()) + == 0) { throw new IllegalArgumentException( "duplicate delegate address " + Address.fromSCAddress(keyedNodes.get(i).getValue().getAddress()).toString()); @@ -848,18 +884,6 @@ private static byte[] scAddressXdrBytes(SCAddress address) { } } - /** Compares two byte arrays lexicographically, treating bytes as unsigned. */ - private static int compareBytes(byte[] a, byte[] b) { - int minLength = Math.min(a.length, b.length); - for (int i = 0; i < minLength; i++) { - int cmp = Integer.compare(a[i] & 0xff, b[i] & 0xff); - if (cmp != 0) { - return cmp; - } - } - return Integer.compare(a.length, b.length); - } - /** * A delegate signer to attach to a {@code SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES} entry via * {@link Auth#buildWithDelegatesEntry(SorobanAuthorizationEntry, long, List, SCVal)} (CAP-71-01). @@ -882,15 +906,18 @@ public static class DelegateSignature { @Nullable List nestedDelegates; } - /** A signature, consisting of a public key and a signature. */ - @Value - public static class Signature { - String publicKey; - byte[] signature; - } - - /** An interface for signing a {@link HashIDPreimage} to produce a signature. */ + /** + * Signs a Soroban authorization preimage, returning the signature {@link SCVal} accepted by the + * account contract at the credential node being signed — the default Stellar Account shape for a + * classic {@code G...} account (see {@link Auth#defaultAccountSignatureScVal(String, byte[])}), + * or whatever the custom account contract's {@code __check_auth} expects (BLS, WebAuthn / + * secp256r1, threshold, policy contracts, ...). + * + *

Use {@link Auth#authorizationPayloadHash(HashIDPreimage)} to obtain the 32-byte payload the + * host hashes from the preimage and hands to {@code __check_auth}. + */ + @FunctionalInterface public interface Signer { - Signature sign(HashIDPreimage preimage); + SCVal sign(HashIDPreimage preimage); } } diff --git a/src/main/java/org/stellar/sdk/Util.java b/src/main/java/org/stellar/sdk/Util.java index 6e3353a73..8c2d3d2d6 100644 --- a/src/main/java/org/stellar/sdk/Util.java +++ b/src/main/java/org/stellar/sdk/Util.java @@ -156,6 +156,27 @@ public static byte[] getBytes(BigInteger value) { return bytes; } + /** + * Compares two byte arrays lexicographically, treating each byte as unsigned. This matches the + * ordering the Soroban host applies to XDR-encoded keys and addresses (e.g. when sorting {@code + * SCMap} entries or delegate addresses). + * + * @param a the first byte array + * @param b the second byte array + * @return a negative integer, zero, or a positive integer as {@code a} is less than, equal to, or + * greater than {@code b} + */ + public static int compareBytesUnsigned(byte[] a, byte[] b) { + int minLength = Math.min(a.length, b.length); + for (int i = 0; i < minLength; i++) { + int cmp = Integer.compare(a[i] & 0xff, b[i] & 0xff); + if (cmp != 0) { + return cmp; + } + } + return Integer.compare(a.length, b.length); + } + /** The function that converts XDR string to XDR object. */ @FunctionalInterface public interface XdrDecodeFunction { diff --git a/src/main/java/org/stellar/sdk/scval/ScvComparator.java b/src/main/java/org/stellar/sdk/scval/ScvComparator.java index 1af839ab0..2b504a787 100644 --- a/src/main/java/org/stellar/sdk/scval/ScvComparator.java +++ b/src/main/java/org/stellar/sdk/scval/ScvComparator.java @@ -1,6 +1,7 @@ package org.stellar.sdk.scval; import java.util.Comparator; +import org.stellar.sdk.Util; import org.stellar.sdk.xdr.ContractExecutable; import org.stellar.sdk.xdr.SCAddress; import org.stellar.sdk.xdr.SCMap; @@ -161,12 +162,12 @@ static int compareScVal(SCVal a, SCVal b) { .compareTo(b.getI256().getLo_lo().getUint64().getNumber()); } case SCV_BYTES: - return compareByteArrays(a.getBytes().getSCBytes(), b.getBytes().getSCBytes()); + return Util.compareBytesUnsigned(a.getBytes().getSCBytes(), b.getBytes().getSCBytes()); case SCV_STRING: - return compareByteArrays( + return Util.compareBytesUnsigned( a.getStr().getSCString().getBytes(), b.getStr().getSCString().getBytes()); case SCV_SYMBOL: - return compareByteArrays( + return Util.compareBytesUnsigned( a.getSym().getSCSymbol().getBytes(), b.getSym().getSCSymbol().getBytes()); case SCV_VEC: { @@ -222,11 +223,11 @@ static int compareScAddress(SCAddress a, SCAddress b) { switch (a.getDiscriminant()) { case SC_ADDRESS_TYPE_ACCOUNT: - return compareByteArrays( + return Util.compareBytesUnsigned( a.getAccountId().getAccountID().getEd25519().getUint256(), b.getAccountId().getAccountID().getEd25519().getUint256()); case SC_ADDRESS_TYPE_CONTRACT: - return compareByteArrays( + return Util.compareBytesUnsigned( a.getContractId().getContractID().getHash(), b.getContractId().getContractID().getHash()); case SC_ADDRESS_TYPE_MUXED_ACCOUNT: @@ -238,7 +239,7 @@ static int compareScAddress(SCAddress a, SCAddress b) { .getNumber() .compareTo(b.getMuxedAccount().getId().getUint64().getNumber()); if (cmp != 0) return cmp; - return compareByteArrays( + return Util.compareBytesUnsigned( a.getMuxedAccount().getEd25519().getUint256(), b.getMuxedAccount().getEd25519().getUint256()); } @@ -249,11 +250,11 @@ static int compareScAddress(SCAddress a, SCAddress b) { "Unsupported ClaimableBalanceID type: " + a.getClaimableBalanceId().getDiscriminant()); } - return compareByteArrays( + return Util.compareBytesUnsigned( a.getClaimableBalanceId().getV0().getHash(), b.getClaimableBalanceId().getV0().getHash()); case SC_ADDRESS_TYPE_LIQUIDITY_POOL: - return compareByteArrays( + return Util.compareBytesUnsigned( a.getLiquidityPoolId().getPoolID().getHash(), b.getLiquidityPoolId().getPoolID().getHash()); default: @@ -267,7 +268,7 @@ static int compareContractExecutable(ContractExecutable a, ContractExecutable b) switch (a.getDiscriminant()) { case CONTRACT_EXECUTABLE_WASM: - return compareByteArrays(a.getWasm_hash().getHash(), b.getWasm_hash().getHash()); + return Util.compareBytesUnsigned(a.getWasm_hash().getHash(), b.getWasm_hash().getHash()); case CONTRACT_EXECUTABLE_STELLAR_ASSET: return 0; default: @@ -293,14 +294,4 @@ private static int compareMapEntries(SCMapEntry[] am, SCMapEntry[] bm) { } return Integer.compare(am.length, bm.length); } - - /** Lexicographic unsigned byte comparison. */ - private static int compareByteArrays(byte[] a, byte[] b) { - int len = Math.min(a.length, b.length); - for (int i = 0; i < len; i++) { - int cmp = Integer.compare(a[i] & 0xFF, b[i] & 0xFF); - if (cmp != 0) return cmp; - } - return Integer.compare(a.length, b.length); - } } diff --git a/src/test/java/org/stellar/sdk/AuthTest.java b/src/test/java/org/stellar/sdk/AuthTest.java index cdb321398..96a65a9b9 100644 --- a/src/test/java/org/stellar/sdk/AuthTest.java +++ b/src/test/java/org/stellar/sdk/AuthTest.java @@ -1,5 +1,6 @@ package org.stellar.sdk; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; @@ -8,6 +9,7 @@ import static org.junit.Assert.fail; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; @@ -218,17 +220,9 @@ public void testSignAuthorizeEntryWithBase64EntryAndFunctionSigner() throws IOEx KeyPair signer = KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); Auth.Signer entrySigner = - preimage -> { - byte[] data; - try { - data = preimage.toXdrByteArray(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to convert preimage to bytes", e); - } - byte[] payload = Util.hash(data); - byte[] signature = signer.sign(payload); - return new Auth.Signature(signer.getAccountId(), signature); - }; + preimage -> + Auth.defaultAccountSignatureScVal( + signer.getAccountId(), signer.sign(Auth.authorizationPayloadHash(preimage))); long validUntilLedgerSeq = 654656L; Network network = Network.TESTNET; @@ -319,17 +313,9 @@ public void testSignAuthorizeEntryWithXdrEntryAndFunctionSigner() throws IOExcep KeyPair signer = KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); Auth.Signer entrySigner = - preimage -> { - byte[] data; - try { - data = preimage.toXdrByteArray(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to convert preimage to bytes", e); - } - byte[] payload = Util.hash(data); - byte[] signature = signer.sign(payload); - return new Auth.Signature(signer.getAccountId(), signature); - }; + preimage -> + Auth.defaultAccountSignatureScVal( + signer.getAccountId(), signer.sign(Auth.authorizationPayloadHash(preimage))); long validUntilLedgerSeq = 654656L; Network network = Network.TESTNET; @@ -454,56 +440,25 @@ public void testSignAuthorizeEntryWithSourceCredentialsEntry() { } @Test - public void testSignAuthorizeEntryWithSignatureMismatchThrows() throws IOException { - String contractId = "CDCYWK73YTYFJZZSJ5V7EDFNHYBG4QN3VUNG2IGD27KJDDPNCZKBCBXK"; + public void testSignAuthorizeEntryWithNullSignatureThrows() { KeyPair signer = KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); - Auth.Signer entrySigner = - preimage -> { - byte[] invalidData = new byte[20]; - byte[] signature = signer.sign(invalidData); - return new Auth.Signature(signer.getAccountId(), signature); - }; - long validUntilLedgerSeq = 654656L; - Network network = Network.TESTNET; - - SorobanCredentials credentials = - SorobanCredentials.builder() - .discriminant(SorobanCredentialsType.SOROBAN_CREDENTIALS_ADDRESS) - .address( - SorobanAddressCredentials.builder() - .address(new Address(KeyPair.random().getAccountId()).toSCAddress()) - .nonce(new Int64(123456789L)) - .signatureExpirationLedger(new Uint32(new XdrUnsignedInteger(0L))) - .signature(Scv.toVoid()) - .build()) - .build(); - SorobanAuthorizedInvocation invocation = - SorobanAuthorizedInvocation.builder() - .function( - SorobanAuthorizedFunction.builder() - .discriminant( - SorobanAuthorizedFunctionType.SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN) - .contractFn( - InvokeContractArgs.builder() - .contractAddress(new Address(contractId).toSCAddress()) - .functionName(Scv.toSymbol("increment").getSym()) - .args(new SCVal[0]) - .build()) - .build()) - .subInvocations(new SorobanAuthorizedInvocation[0]) - .build(); + // A Signer may now return any signature SCVal, attached verbatim with no client-side + // verification (the network checks it via __check_auth). A null return is the one thing we + // can sanity-check. + Auth.Signer nullSigner = preimage -> null; SorobanAuthorizationEntry entry = SorobanAuthorizationEntry.builder() - .credentials(credentials) - .rootInvocation(invocation) + .credentials( + buildAddressCredentials(new Address(signer.getAccountId()).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) .build(); try { - Auth.authorizeEntry(entry.toXdrBase64(), entrySigner, validUntilLedgerSeq, network); + Auth.authorizeEntry(entry, nullSigner, 654656L, Network.TESTNET); fail(); } catch (IllegalArgumentException e) { - assertEquals("signature does not match payload", e.getMessage()); + assertEquals("signer returned a null signature", e.getMessage()); } } @@ -563,17 +518,9 @@ public void authorizeInvocationWithFunctionSigner() { KeyPair signer = KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); Auth.Signer entrySigner = - preimage -> { - byte[] data; - try { - data = preimage.toXdrByteArray(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to convert preimage to bytes", e); - } - byte[] payload = Util.hash(data); - byte[] signature = signer.sign(payload); - return new Auth.Signature(signer.getAccountId(), signature); - }; + preimage -> + Auth.defaultAccountSignatureScVal( + signer.getAccountId(), signer.sign(Auth.authorizationPayloadHash(preimage))); long validUntilLedgerSeq = 654656L; Network network = Network.TESTNET; @@ -718,17 +665,9 @@ public void testSignAuthorizeEntryWithFunctionSignerNotEqualCredentialAddress() KeyPair signer = KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); Auth.Signer entrySigner = - preimage -> { - byte[] data; - try { - data = preimage.toXdrByteArray(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to convert preimage to bytes", e); - } - byte[] payload = Util.hash(data); - byte[] signature = signer.sign(payload); - return new Auth.Signature(signer.getAccountId(), signature); - }; + preimage -> + Auth.defaultAccountSignatureScVal( + signer.getAccountId(), signer.sign(Auth.authorizationPayloadHash(preimage))); long validUntilLedgerSeq = 654656L; Network network = Network.TESTNET; String credentialAddress = "GADBBY4WFXKKFJ7CMTG3J5YAUXMQDBILRQ6W3U5IWN5TQFZU4MWZ5T4K"; @@ -1000,13 +939,8 @@ public void testBuildAuthorizationEntryPreimageMatchesAuthorizeEntryPayload() { Auth.Signer entrySigner = preimage -> { captured[0] = preimage; - byte[] data; - try { - data = preimage.toXdrByteArray(); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to convert preimage to bytes", e); - } - return new Auth.Signature(signer.getAccountId(), signer.sign(Util.hash(data))); + return Auth.defaultAccountSignatureScVal( + signer.getAccountId(), signer.sign(Auth.authorizationPayloadHash(preimage))); }; Auth.authorizeEntry(entry, entrySigner, validUntilLedgerSeq, network); @@ -1554,6 +1488,189 @@ public void authorizeInvocationWithUnsupportedCredentialsTypeThrows() { } } + @Test + public void testSignAuthorizeEntryWithCustomContractSignature() { + // A custom account contract whose __check_auth expects an arbitrary structure. The signer + // returns that SCVal and it is attached verbatim — no ed25519 wrapping, no client-side + // verification. + String contractId = "CDCYWK73YTYFJZZSJ5V7EDFNHYBG4QN3VUNG2IGD27KJDDPNCZKBCBXK"; + long validUntilLedgerSeq = 654656L; + Network network = Network.TESTNET; + + SCVal customSignature = + Scv.toMap( + new LinkedHashMap() { + { + put( + Scv.toSymbol("bls"), + Scv.toBytes("bls-signature".getBytes(StandardCharsets.UTF_8))); + put( + Scv.toSymbol("webauthn"), + Scv.toMap( + new LinkedHashMap() { + { + put( + Scv.toSymbol("authenticator_data"), + Scv.toBytes("authenticator-data".getBytes(StandardCharsets.UTF_8))); + put( + Scv.toSymbol("client_data_json"), + Scv.toBytes( + "{\"type\":\"webauthn.get\"}" + .getBytes(StandardCharsets.UTF_8))); + put( + Scv.toSymbol("signature"), + Scv.toBytes("webauthn-signature".getBytes(StandardCharsets.UTF_8))); + } + })); + } + }); + + SorobanAuthorizationEntry entry = + SorobanAuthorizationEntry.builder() + .credentials(buildAddressCredentials(new Address(contractId).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + + HashIDPreimage[] seen = new HashIDPreimage[1]; + int[] calls = {0}; + Auth.Signer signer = + preimage -> { + seen[0] = preimage; + calls[0]++; + return customSignature; + }; + + SorobanAuthorizationEntry signedEntry = + Auth.authorizeEntry(entry, signer, validUntilLedgerSeq, network); + + SorobanAddressCredentials credentials = signedEntry.getCredentials().getAddress(); + assertEquals(customSignature, credentials.getSignature()); + assertEquals( + validUntilLedgerSeq, + credentials.getSignatureExpirationLedger().getUint32().getNumber().longValue()); + // The signer is invoked exactly once, with the same preimage authorizeEntry builds. + assertEquals(1, calls[0]); + assertEquals( + Auth.buildAuthorizationEntryPreimage(signedEntry, validUntilLedgerSeq, network), seen[0]); + } + + @Test + public void testSignAuthorizeInvocationWithContractAddressAndCustomSignature() { + // A custom (non-KeyPair) signer authorizing an invocation for a C... contract address. + String contractId = "CDCYWK73YTYFJZZSJ5V7EDFNHYBG4QN3VUNG2IGD27KJDDPNCZKBCBXK"; + long validUntilLedgerSeq = 654656L; + Network network = Network.TESTNET; + + SCVal customSignature = + Scv.toVec( + Arrays.asList( + Scv.toSymbol("passkey"), + Scv.toBytes("webauthn-signature".getBytes(StandardCharsets.UTF_8)))); + + Auth.Signer signer = preimage -> customSignature; + SorobanAuthorizationEntry signedEntry = + Auth.authorizeInvocation( + signer, contractId, validUntilLedgerSeq, buildInvocation(), network); + + SorobanAddressCredentials credentials = signedEntry.getCredentials().getAddress(); + assertEquals(new Address(contractId).toSCAddress(), credentials.getAddress()); + assertEquals(customSignature, credentials.getSignature()); + } + + @Test + public void testForAddressWritesCustomScValIntoDelegateNode() { + // CAP-71-01: a delegate that is itself a custom account contract. The custom signature SCVal is + // written verbatim into the delegate node selected by forAddress — the case that motivated + // returning SCVal from Signer. + KeyPair account = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + String delegateContract = "CDCYWK73YTYFJZZSJ5V7EDFNHYBG4QN3VUNG2IGD27KJDDPNCZKBCBXK"; + long validUntilLedgerSeq = 654656L; + Network network = Network.TESTNET; + + SorobanAuthorizationEntry baseEntry = + SorobanAuthorizationEntry.builder() + .credentials( + buildAddressV2Credentials(new Address(account.getAccountId()).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + SorobanAuthorizationEntry wrapped = + Auth.buildWithDelegatesEntry( + baseEntry, + validUntilLedgerSeq, + Collections.singletonList(new Auth.DelegateSignature(delegateContract, null, null)), + null); + + SCVal customSignature = + Scv.toBytes("delegate-contract-signature".getBytes(StandardCharsets.UTF_8)); + Auth.Signer signer = preimage -> customSignature; + + SorobanAuthorizationEntry signedEntry = + Auth.authorizeEntry(wrapped, signer, validUntilLedgerSeq, network, delegateContract); + + SorobanAddressCredentialsWithDelegates withDelegates = + signedEntry.getCredentials().getAddressWithDelegates(); + // Top-level node untouched (we targeted the delegate) ... + assertEquals( + SCValType.SCV_VOID, withDelegates.getAddressCredentials().getSignature().getDiscriminant()); + // ... the delegate node carries the custom SCVal verbatim. + SorobanDelegateSignature[] delegates = withDelegates.getDelegates(); + assertEquals(1, delegates.length); + assertEquals(new Address(delegateContract).toSCAddress(), delegates[0].getAddress()); + assertEquals(customSignature, delegates[0].getSignature()); + } + + @Test + public void testDefaultAccountSignatureScVal() { + KeyPair signer = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + byte[] signature = new byte[64]; + Arrays.fill(signature, (byte) 7); + // Byte-identical to the hand-built default Stellar Account shape used elsewhere in this suite, + // from both the accountId and the raw-publicKey overloads. + SCVal expected = buildSignatureScVal(signer, signature); + assertEquals(expected, Auth.defaultAccountSignatureScVal(signer.getAccountId(), signature)); + assertEquals(expected, Auth.defaultAccountSignatureScVal(signer.getPublicKey(), signature)); + } + + @Test + public void testAuthorizationPayloadHash() throws IOException { + KeyPair signer = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + SorobanAuthorizationEntry entry = + SorobanAuthorizationEntry.builder() + .credentials( + buildAddressV2Credentials(new Address(signer.getAccountId()).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + HashIDPreimage preimage = Auth.buildAuthorizationEntryPreimage(entry, 654656L, Network.TESTNET); + assertArrayEquals( + Util.hash(preimage.toXdrByteArray()), Auth.authorizationPayloadHash(preimage)); + } + + @Test + public void testAuthorizeEntryWithCustomSignatureMatchesVector() throws IOException { + // A fixed vector pinning byte-stable output: with these inputs (contract address credentials, + // nonce 123456789, expiration 654656, TESTNET, "increment" invocation) and a deterministic + // custom signature SCVal, authorizeEntry attaches it to produce this exact signed entry. No + // crypto is involved — the SDK attaches, the host verifies. + String contractId = "CDCYWK73YTYFJZZSJ5V7EDFNHYBG4QN3VUNG2IGD27KJDDPNCZKBCBXK"; + String customSignatureVector = + "AAAAAQAAAAHFiyv7xPBU5zJPa/IMrT4CbkG7rRptIMPX1JGN7RZUEQAAAAAHW80VAAn9QAAAAA0AAAASd2ViYXV0aG4tc2lnbmF0dXJlAAAAAAAAAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAACWluY3JlbWVudAAAAAAAAAAAAAAA"; + + SorobanAuthorizationEntry entry = + SorobanAuthorizationEntry.builder() + .credentials(buildAddressCredentials(new Address(contractId).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + SCVal customSignature = Scv.toBytes("webauthn-signature".getBytes(StandardCharsets.UTF_8)); + + assertEquals( + customSignatureVector, + Auth.authorizeEntry(entry, preimage -> customSignature, 654656L, Network.TESTNET) + .toXdrBase64()); + } + private static KeyPair deterministicKeyPair(byte fill) { byte[] seed = new byte[32]; Arrays.fill(seed, fill); diff --git a/src/test/kotlin/org/stellar/sdk/UtilTest.kt b/src/test/kotlin/org/stellar/sdk/UtilTest.kt index c31e710ce..3bceced16 100644 --- a/src/test/kotlin/org/stellar/sdk/UtilTest.kt +++ b/src/test/kotlin/org/stellar/sdk/UtilTest.kt @@ -2,6 +2,8 @@ package org.stellar.sdk import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.ints.shouldBeNegative +import io.kotest.matchers.ints.shouldBePositive import io.kotest.matchers.shouldBe class UtilTest : @@ -96,4 +98,30 @@ class UtilTest : Util.bytesToHex(bytes).lowercase() shouldBe hex } } + + context("compareBytesUnsigned") { + test("equal arrays compare equal") { + Util.compareBytesUnsigned(byteArrayOf(1, 2, 3), byteArrayOf(1, 2, 3)) shouldBe 0 + Util.compareBytesUnsigned(byteArrayOf(), byteArrayOf()) shouldBe 0 + } + + test("bytes are treated as unsigned") { + // 0x80 (128) sorts after 0x7F (127); a signed-byte comparison would invert this. + Util.compareBytesUnsigned(byteArrayOf(0x80.toByte()), byteArrayOf(0x7F)).shouldBePositive() + Util.compareBytesUnsigned(byteArrayOf(0x7F), byteArrayOf(0x80.toByte())).shouldBeNegative() + // 0xFF (255) is the largest single byte value. + Util.compareBytesUnsigned(byteArrayOf(0xFF.toByte()), byteArrayOf(0x00)).shouldBePositive() + } + + test("shorter array is less when it is a prefix of the longer") { + Util.compareBytesUnsigned(byteArrayOf(1, 2), byteArrayOf(1, 2, 3)).shouldBeNegative() + Util.compareBytesUnsigned(byteArrayOf(1, 2, 3), byteArrayOf(1, 2)).shouldBePositive() + Util.compareBytesUnsigned(byteArrayOf(), byteArrayOf(1)).shouldBeNegative() + } + + test("first differing byte decides regardless of length") { + Util.compareBytesUnsigned(byteArrayOf(1, 0xFF.toByte()), byteArrayOf(2)).shouldBeNegative() + Util.compareBytesUnsigned(byteArrayOf(2), byteArrayOf(1, 2, 3)).shouldBePositive() + } + } }) From cf975ea890039c8f2a78be601be2536a66d3cda0 Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Thu, 18 Jun 2026 15:01:18 +0800 Subject: [PATCH 2/3] fix --- CHANGELOG.md | 7 ++++++- src/main/java/org/stellar/sdk/Auth.java | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6d73467..0b21e0fed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,12 @@ // before Auth.Signer signer = preimage -> { - byte[] payload = Util.hash(preimage.toXdrByteArray()); + byte[] payload; + try { + payload = Util.hash(preimage.toXdrByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } return new Auth.Signature(accountId, signRemotely(payload)); }; diff --git a/src/main/java/org/stellar/sdk/Auth.java b/src/main/java/org/stellar/sdk/Auth.java index 743738117..8d105d4d8 100644 --- a/src/main/java/org/stellar/sdk/Auth.java +++ b/src/main/java/org/stellar/sdk/Auth.java @@ -56,7 +56,7 @@ public class Auth { * @param signer a {@link KeyPair} which should correspond to the address in the `entry` * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @return a signed Soroban authorization entry */ public static SorobanAuthorizationEntry authorizeEntry( @@ -83,7 +83,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * @param signer a {@link KeyPair} which should correspond to the address in the `entry` * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @param forAddress which credential node the signature should be written to, see {@link * Auth#authorizeEntry(SorobanAuthorizationEntry, Signer, Long, Network, String)} * @return a signed Soroban authorization entry @@ -122,7 +122,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * @param signer a {@link KeyPair} which should correspond to the address in the `entry` * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @return a signed Soroban authorization entry */ public static SorobanAuthorizationEntry authorizeEntry( @@ -149,7 +149,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * @param signer a {@link KeyPair} which should correspond to the address in the `entry` * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @param forAddress which credential node the signature should be written to, see {@link * Auth#authorizeEntry(SorobanAuthorizationEntry, Signer, Long, Network, String)} * @return a signed Soroban authorization entry @@ -188,7 +188,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @return a signed Soroban authorization entry */ public static SorobanAuthorizationEntry authorizeEntry( @@ -216,7 +216,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @param forAddress which credential node the signature should be written to, see {@link * Auth#authorizeEntry(SorobanAuthorizationEntry, Signer, Long, Network, String)} * @return a signed Soroban authorization entry @@ -256,7 +256,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @return a signed Soroban authorization entry */ public static SorobanAuthorizationEntry authorizeEntry( @@ -290,7 +290,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * and returns the signature {@link SCVal} the account at the entry's address expects * @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @param forAddress which credential node the signature should be written to. Only relevant for * {@code SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES}, where a single entry can be signed by * the top-level account and/or any of its (possibly nested) delegates. Per CAP-71-01 every @@ -387,7 +387,7 @@ public static SorobanAuthorizationEntry authorizeEntry( * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param invocation invocation the invocation tree that we're authorizing (likely, this comes * from transaction simulation) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @return a signed Soroban authorization entry */ public static SorobanAuthorizationEntry authorizeInvocation( @@ -422,7 +422,7 @@ public static SorobanAuthorizationEntry authorizeInvocation( * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param invocation invocation the invocation tree that we're authorizing (likely, this comes * from transaction simulation) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @param credentialsType the credential type for the new entry, either the legacy {@code * SOROBAN_CREDENTIALS_ADDRESS} (the default of the shorter overloads, valid on every network) * or the address-bound {@code SOROBAN_CREDENTIALS_ADDRESS_V2} (CAP-71-02, requires a protocol @@ -477,7 +477,7 @@ public static SorobanAuthorizationEntry authorizeInvocation( * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param invocation invocation the invocation tree that we're authorizing (likely, this comes * from transaction simulation) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @return a signed Soroban authorization entry */ public static SorobanAuthorizationEntry authorizeInvocation( @@ -517,7 +517,7 @@ public static SorobanAuthorizationEntry authorizeInvocation( * authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired) * @param invocation invocation the invocation tree that we're authorizing (likely, this comes * from transaction simulation) - * @param network the network is incorprated into the signature + * @param network the network is incorporated into the signature * @param credentialsType the credential type for the new entry, either the legacy {@code * SOROBAN_CREDENTIALS_ADDRESS} (the default of the shorter overloads, valid on every network) * or the address-bound {@code SOROBAN_CREDENTIALS_ADDRESS_V2} (CAP-71-02, requires a protocol From 7e5dcd01806d6f994a5a4063b9aba8f619e7154b Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Thu, 18 Jun 2026 15:03:09 +0800 Subject: [PATCH 3/3] fix --- CHANGELOG.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b21e0fed..6d6d73467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,12 +17,7 @@ // before Auth.Signer signer = preimage -> { - byte[] payload; - try { - payload = Util.hash(preimage.toXdrByteArray()); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } + byte[] payload = Util.hash(preimage.toXdrByteArray()); return new Auth.Signature(accountId, signRemotely(payload)); };