Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update XDR for Protocol 19, add encoding support for CAP-40 SignedPayload Signer #413

Closed
wants to merge 7 commits into from
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

As this project is pre 1.0, breaking changes may happen for minor version bumps. A breaking change will get clearly notified in this log.

## 0.32.0 (Pending)

* Update XDR definitions and auto-generated classes to support upcoming protocol 19 release ([#276](https://github.com/stellar/java-stellar-sdk/pull/276)).
* Extend StrKey implementation to handle [CAP 40 Payload Signer](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0040.md) ([#276](https://github.com/stellar/java-stellar-sdk/pull/276)).

### Breaking changes

* Predicate.AbsBefore
* `Predicate.AbsBefore(org.stellar.sdk.xdr.TimePoint)`
* `org.stellar.sdk.xdr.TimePoint Predicate.AbsBefore.getTimestampSeconds()`
* Predicate.RelBefore
* `Predicate.RelBefore(org.stellar.sdk.xdr.Duration)`
* `org.stellar.sdk.xdr.Duration Predicate.RelBefore.getSecondsSinceClose()`

## 0.31.0

* Fixed NPE on TrustlineCreatedEffectResponse.getAsset() for liquidity pool asset type.
Expand Down
6 changes: 6 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ For information on how to contribute, please refer to our [contribution guide](h

## License
java-stellar-sdk is licensed under an Apache-2.0 license. See the [LICENSE](https://github.com/stellar/java-stellar-sdk/blob/master/LICENSE) file for details.

## xdr to jave code generation
All the java source files in org.stellar.sdk.xdr package are generated using the `xdrgen` command from the [stellar/xdrgen](https://github.com/stellar/xdrgen)
```
xdrgen -o ./src/main/java/org/stellar/sdk/xdr -l java -n org.stellar.sdk.xdr ./xdr/Stellar-types.x ./xdr/Stellar-SCP.x ./xdr/Stellar-overlay.x ./xdr/Stellar-ledger-entries.x ./xdr/Stellar-ledger.x ./xdr/Stellar-transaction.x
```
52 changes: 32 additions & 20 deletions src/main/java/org/stellar/sdk/Predicate.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import com.google.common.collect.Lists;
import org.stellar.sdk.xdr.ClaimPredicate;
import org.stellar.sdk.xdr.ClaimPredicateType;
import org.stellar.sdk.xdr.Duration;
import org.stellar.sdk.xdr.Int64;
import org.stellar.sdk.xdr.TimePoint;
import org.stellar.sdk.xdr.Uint64;
import org.threeten.bp.Instant;

import java.util.List;
Expand Down Expand Up @@ -34,9 +37,9 @@ public static Predicate fromXdr(org.stellar.sdk.xdr.ClaimPredicate xdr) {
case CLAIM_PREDICATE_NOT:
return new Not(fromXdr(xdr.getNotPredicate()));
case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME:
return new RelBefore(xdr.getRelBefore().getInt64());
return new RelBefore(xdr.getRelBefore());
case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME:
return new AbsBefore(xdr.getAbsBefore().getInt64());
return new AbsBefore(xdr.getAbsBefore());
default:
throw new IllegalArgumentException("Unknown asset type " + xdr.getDiscriminant());
}
Expand Down Expand Up @@ -177,76 +180,85 @@ public ClaimPredicate toXdr() {
}
}

/**
* Represents a predicate based on a maximum date and time.
*/
public static class AbsBefore extends Predicate {
private final long epochSeconds;
private final TimePoint timePoint;

public AbsBefore(long epochSeconds) {
this.epochSeconds = epochSeconds;
public AbsBefore(TimePoint timePoint) {
this.timePoint = timePoint;
}

public AbsBefore(long epochSeconds) {
this(new TimePoint(new Uint64(epochSeconds)));
}
public long getTimestampSeconds() {
return epochSeconds;
return timePoint.getTimePoint().getUint64();
}

public Instant getDate() {
return Instant.ofEpochSecond(epochSeconds);
return Instant.ofEpochSecond(timePoint.getTimePoint().getUint64());
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
return (getClass() == o.getClass()) && Objects.equal(epochSeconds, ((AbsBefore)o).epochSeconds);
return (getClass() == o.getClass()) && Objects.equal(timePoint, ((AbsBefore)o).timePoint);
}

@Override
public int hashCode() {
return Objects.hashCode(epochSeconds);
return Objects.hashCode(timePoint);
}

@Override
public ClaimPredicate toXdr() {
org.stellar.sdk.xdr.ClaimPredicate xdr = new org.stellar.sdk.xdr.ClaimPredicate();
xdr.setDiscriminant(ClaimPredicateType.CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME);
Int64 t = new Int64();
t.setInt64(epochSeconds);
xdr.setAbsBefore(t);
xdr.setAbsBefore(timePoint);
return xdr;
}
}

/**
* Represents predicate based on maximum length of time
*/
public static class RelBefore extends Predicate {
private final long secondsSinceClose;
private final Duration duration;

public RelBefore(Duration secondsSinceClose) {
this.duration = secondsSinceClose;
}

public RelBefore(long secondsSinceClose) {
this.secondsSinceClose = secondsSinceClose;
this(new Duration(new Int64(secondsSinceClose)));
}

public long getSecondsSinceClose() {
return secondsSinceClose;
return duration.getDuration().getInt64();
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
return (getClass() == o.getClass()) && Objects.equal(secondsSinceClose, ((RelBefore)o).secondsSinceClose);
return (getClass() == o.getClass()) && Objects.equal(duration, ((RelBefore)o).duration);
}

@Override
public int hashCode() {
return Objects.hashCode(secondsSinceClose);
return Objects.hashCode(duration);
}

@Override
public ClaimPredicate toXdr() {
org.stellar.sdk.xdr.ClaimPredicate xdr = new org.stellar.sdk.xdr.ClaimPredicate();
xdr.setDiscriminant(ClaimPredicateType.CLAIM_PREDICATE_BEFORE_RELATIVE_TIME);
Int64 t = new Int64();
t.setInt64(secondsSinceClose);
xdr.setRelBefore(t);
xdr.setRelBefore(duration);
return xdr;
}
}
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/org/stellar/sdk/SignedPayloadSigner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.stellar.sdk;

import org.stellar.sdk.xdr.AccountID;
import org.stellar.sdk.xdr.PublicKey;
import org.stellar.sdk.xdr.PublicKeyType;
import org.stellar.sdk.xdr.Uint256;

import static com.google.common.base.Preconditions.checkNotNull;

/**
* Data model for the <a href="https://github.com/stellar/stellar-protocol/blob/master/core/cap-0040.md#xdr-changes">signed payload signer </a>
*/
public class SignedPayloadSigner {
public static final int SIGNED_PAYLOAD_MAX_PAYLOAD_LENGTH = 64;

private AccountID signerAccountId;
private byte[] payload;

/**
* constructor
*
* @param signerAccountId - the xdr AccountID
* @param payload - the raw payload for a signed payload
*/
public SignedPayloadSigner(AccountID signerAccountId, byte[] payload) {
checkNotNull(payload, "payload cannot be null");
checkNotNull(signerAccountId, "accountId cannot be null");
if (payload.length > SIGNED_PAYLOAD_MAX_PAYLOAD_LENGTH) {
throw new IllegalArgumentException("invalid payload length, must be less than " + SIGNED_PAYLOAD_MAX_PAYLOAD_LENGTH);
}
if (signerAccountId.getAccountID().getDiscriminant() == null ||
!signerAccountId.getAccountID().getDiscriminant().equals(PublicKeyType.PUBLIC_KEY_TYPE_ED25519)) {
throw new IllegalArgumentException("invalid payload signer, only ED25519 public key accounts are supported currently");
}
this.signerAccountId = checkNotNull(signerAccountId);
this.payload = checkNotNull(payload);
sreuland marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* constructor
*
* @param signerED25519PublicKey raw bytes of a ED25519 public key for the signer account
* @param payload the raw payload for a signed payload
*/
public SignedPayloadSigner(byte[] signerED25519PublicKey, byte[] payload ) {
this(new AccountID(
new PublicKey.Builder()
.discriminant(PublicKeyType.PUBLIC_KEY_TYPE_ED25519)
.ed25519(new Uint256(signerED25519PublicKey)).build()), payload);
}

/**
* get the account that represents the signed payload signer
* @return stellar account
*/
public AccountID getSignerAccountId() {
return signerAccountId;
}
sreuland marked this conversation as resolved.
Show resolved Hide resolved

/**
* get the payload that signatures are produced from.
* @see <a href="https://github.com/stellar/stellar-protocol/blob/master/core/cap-0040.md#semantics"/>
* @return
*/
public byte[] getPayload() {
return payload;
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/stellar/sdk/Signer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.stellar.sdk.xdr.SignerKeyType;
import org.stellar.sdk.xdr.Uint256;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
Expand Down Expand Up @@ -72,6 +73,25 @@ public static SignerKey preAuthTx(byte[] hash) {
return signerKey;
}

/**
* Create <code>SignerKey</code> {@link org.stellar.sdk.xdr.SignerKey} from {@link org.stellar.sdk.SignedPayloadSigner}
*
* @param signedPayloadSigner - signed payload values
* @return org.stellar.sdk.xdr.SignerKey
*/
public static SignerKey signedPayload(SignedPayloadSigner signedPayloadSigner) {

SignerKey signerKey = new SignerKey();
SignerKey.SignerKeyEd25519SignedPayload payloadSigner = new SignerKey.SignerKeyEd25519SignedPayload();
payloadSigner.setPayload(signedPayloadSigner.getPayload());
payloadSigner.setEd25519(signedPayloadSigner.getSignerAccountId().getAccountID().getEd25519());

signerKey.setDiscriminant(SignerKeyType.SIGNER_KEY_TYPE_ED25519_SIGNED_PAYLOAD);
signerKey.setEd25519SignedPayload(payloadSigner);

return signerKey;
}

private static Uint256 createUint256(byte[] hash) {
if (hash.length != 32) {
throw new RuntimeException("hash must be 32 bytes long");
Expand Down
66 changes: 55 additions & 11 deletions src/main/java/org/stellar/sdk/StrKey.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package org.stellar.sdk;

import com.google.common.io.BaseEncoding;
import com.google.common.base.Optional;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import org.stellar.sdk.xdr.*;

import java.io.*;
import org.stellar.sdk.xdr.AccountID;
import org.stellar.sdk.xdr.CryptoKeyType;
import org.stellar.sdk.xdr.MuxedAccount;
import org.stellar.sdk.xdr.PublicKey;
import org.stellar.sdk.xdr.PublicKeyType;
import org.stellar.sdk.xdr.SignerKey;
import org.stellar.sdk.xdr.Uint256;
import org.stellar.sdk.xdr.Uint64;
import org.stellar.sdk.xdr.XdrDataInputStream;
import org.stellar.sdk.xdr.XdrDataOutputStream;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;

class StrKey {
Expand All @@ -18,7 +31,8 @@ public enum VersionByte {
MUXED((byte)(12 << 3)), // M
SEED((byte)(18 << 3)), // S
PRE_AUTH_TX((byte)(19 << 3)), // T
SHA256_HASH((byte)(23 << 3)); // X
SHA256_HASH((byte)(23 << 3)), // X
SIGNED_PAYLOAD((byte)(15 << 3)); // P
private final byte value;
VersionByte(byte value) {
this.value = value;
Expand Down Expand Up @@ -49,6 +63,22 @@ public static String encodeStellarAccountId(AccountID accountID) {
return String.valueOf(encoded);
}

public static String encodeSignedPayload(SignedPayloadSigner signedPayloadSigner) {
try {
SignerKey.SignerKeyEd25519SignedPayload xdrPayloadSigner = new SignerKey.SignerKeyEd25519SignedPayload();
xdrPayloadSigner.setPayload(signedPayloadSigner.getPayload());
xdrPayloadSigner.setEd25519(signedPayloadSigner.getSignerAccountId().getAccountID().getEd25519());

ByteArrayOutputStream record = new ByteArrayOutputStream();
xdrPayloadSigner.encode(new XdrDataOutputStream(record));

char[] encoded = encodeCheck(VersionByte.SIGNED_PAYLOAD, record.toByteArray());
return String.valueOf(encoded);
} catch (Exception ex) {
throw new FormatException(ex.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

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

Why rethrow when you can just let it bubble up?

Copy link
Contributor Author

@sreuland sreuland Mar 23, 2022

Choose a reason for hiding this comment

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

to muffle the checked exceptions which otherwise would require adding on a throws declaration on the method for each one, which then forces the callers to specifically handle it, it's mostly for caller convenience as there is no significant app-level meaning to a checked failure that the client could catch and do something else instead, rather they should just let it go up also.

Copy link
Contributor

Choose a reason for hiding this comment

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

makes sense, java-isms.. 👍

}
}

public static String encodeStellarMuxedAccount(MuxedAccount muxedAccount) {
switch (muxedAccount.getDiscriminant()) {
case KEY_TYPE_MUXED_ED25519:
Expand Down Expand Up @@ -154,6 +184,20 @@ public static byte[] decodeStellarSecretSeed(char[] data) {
return decodeCheck(VersionByte.SEED, data);
}

public static SignedPayloadSigner decodeSignedPayload(char[] data) {
try {
byte[] signedPayloadRaw = decodeCheck(VersionByte.SIGNED_PAYLOAD, data);

SignerKey.SignerKeyEd25519SignedPayload xdrPayloadSigner = SignerKey.SignerKeyEd25519SignedPayload.decode(
new XdrDataInputStream(new ByteArrayInputStream(signedPayloadRaw))
);

return new SignedPayloadSigner( xdrPayloadSigner.getEd25519().getUint256(), xdrPayloadSigner.getPayload());
} catch (Exception ex) {
throw new FormatException(ex.getMessage());
}
}

public static String encodePreAuthTx(byte[] data) {
char[] encoded = encodeCheck(VersionByte.PRE_AUTH_TX, data);
return String.valueOf(encoded);
Expand All @@ -177,20 +221,20 @@ protected static char[] encodeCheck(VersionByte versionByte, byte[] data) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(versionByte.getValue());
outputStream.write(data);
byte payload[] = outputStream.toByteArray();
byte checksum[] = StrKey.calculateChecksum(payload);
byte[] payload = outputStream.toByteArray();
byte[] checksum = StrKey.calculateChecksum(payload);
outputStream.write(checksum);
byte unencoded[] = outputStream.toByteArray();
byte[] unencoded = outputStream.toByteArray();

if (VersionByte.SEED != versionByte) {
return StrKey.base32Encoding.encode(unencoded).toCharArray();
return base32Encoding.encode(unencoded).toCharArray();
}

// Why not use base32Encoding.encode here?
// We don't want secret seed to be stored as String in memory because of security reasons. It's impossible
// to erase it from memory when we want it to be erased (ASAP).
CharArrayWriter charArrayWriter = new CharArrayWriter(unencoded.length);
OutputStream charOutputStream = StrKey.base32Encoding.encodingStream(charArrayWriter);
OutputStream charOutputStream = base32Encoding.encodingStream(charArrayWriter);
Comment on lines 233 to +237
Copy link
Contributor

Choose a reason for hiding this comment

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

Errm, note the comment above this line? I think there's a reason we use StrKey? Maybe @tamirms knows more on this (or git blame it)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this comment was referring to the usage of .encode() vs .encodingStream(), the change here was just source tidy-up, StrKey.base32Encoding was redundant, as within StrKey class scope, it references the same static member declared here at base32Encoding

charOutputStream.write(unencoded);
char[] charsEncoded = charArrayWriter.toCharArray();

Expand Down Expand Up @@ -242,7 +286,7 @@ protected static byte[] decodeCheck(VersionByte versionByte, char[] encoded) {
}
}

byte[] decoded = StrKey.base32Encoding.decode(java.nio.CharBuffer.wrap(encoded));
byte[] decoded = base32Encoding.decode(java.nio.CharBuffer.wrap(encoded));
byte decodedVersionByte = decoded[0];
byte[] payload = Arrays.copyOfRange(decoded, 0, decoded.length-2);
byte[] data = Arrays.copyOfRange(payload, 1, payload.length);
Expand Down
Loading