Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
.claude
build
src/test/resources/docker/aggregator/mongo-data

# Genesis the integration stack generates per run
src/test/resources/integration/data/
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,5 @@ For questions about the Unicity Labs, visit [unicity-labs.com](https://unicity-l
- Built on the Unicity network protocol
- Uses Jackson for CBOR encoding
- Uses Bouncy Castle for cryptographic operations
- Uses OkHttp for Android-compatible HTTP operations
- Uses OkHttp for Android-compatible HTTP operations

19 changes: 10 additions & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ plugins {
}

group = "org.unicitylabs"
// Use version property if provided, otherwise use default
version = if (project.hasProperty("version")) {
project.property("version").toString()
} else {
"1.1-SNAPSHOT"
}
// Use version property if provided, otherwise use default. hasProperty("version") is always true
// because Gradle defines it, so check for the "unspecified" it holds when -Pversion was not passed.
version = project.property("version")
?.takeIf { it.toString() != "unspecified" }
?.toString()
?: "3.0-SNAPSHOT"

repositories {
mavenCentral()
Expand Down Expand Up @@ -57,9 +57,10 @@ dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.testcontainers:testcontainers:1.19.8")
testImplementation("org.testcontainers:junit-jupiter:1.19.8")
testImplementation("org.testcontainers:mongodb:1.19.8")
// Core only: the junit-jupiter and mongodb modules were declared but never used, and
// Testcontainers 2.x does not publish them. 2.x carries docker-java 3.7.1, which speaks the
// API version Docker 29 requires; 1.19.8 negotiated 1.32 and could not connect at all.
testImplementation("org.testcontainers:testcontainers:2.0.5")
testImplementation("org.awaitility:awaitility:4.2.0")
testImplementation("org.slf4j:slf4j-simple:2.0.13")
testImplementation("com.google.guava:guava:33.0.0-jre")
Expand Down
42 changes: 31 additions & 11 deletions src/main/java/org/unicitylabs/sdk/api/CertificationData.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,39 @@
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
* Certification data.
*/
public class CertificationData {
public static final long CBOR_TAG = 39031;
private static final int VERSION = 1;
/** The only accepted wire version. One version, one element count. */
public static final int VERSION = 2;
private static final int FIELD_COUNT = 6;

private final EncodedPredicate lockScript;
private final DataHash sourceStateHash;
private final DataHash transactionHash;
private final Long expiresAt;
private final byte[] unlockScript;

CertificationData(
EncodedPredicate lockScript,
DataHash sourceStateHash,
DataHash transactionHash,
Long expiresAt,
byte[] unlockScript
) {
this.lockScript = lockScript;
this.sourceStateHash = sourceStateHash;
this.transactionHash = transactionHash;
this.expiresAt = expiresAt;
this.unlockScript = Arrays.copyOf(unlockScript, unlockScript.length);
}

public int getVersion() {
return CertificationData.VERSION;
return VERSION;
}

/**
Expand Down Expand Up @@ -73,6 +79,15 @@ public DataHash getTransactionHash() {
return this.transactionHash;
}

/**
* Get the exclusive certification request deadline in Unix seconds.
*
* @return request deadline, empty when the Unicity Service assigns one
*/
public Optional<Long> getExpiresAt() {
return Optional.ofNullable(this.expiresAt);
}

/**
* Get unlock script used for certification.
*
Expand All @@ -93,18 +108,20 @@ public static CertificationData fromCbor(byte[] bytes) {
if (tag.getTag() != CertificationData.CBOR_TAG) {
throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag()));
}
List<byte[]> data = CborDeserializer.decodeArray(tag.getData(), 5);
List<byte[]> data = CborDeserializer.decodeArray(tag.getData(), FIELD_COUNT);

int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt();
if (version != CertificationData.VERSION) {
if (version != VERSION) {
throw new CborSerializationException(String.format("Unsupported version: %s", version));
}

return new CertificationData(
EncodedPredicate.fromCbor(data.get(1)),
new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(2))),
new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(3))),
CborDeserializer.decodeByteString(data.get(4))
CborDeserializer.decodeNullable(
data.get(4), value -> CborDeserializer.decodeUnsignedInteger(value).asLong()),
CborDeserializer.decodeByteString(data.get(5))
);
}

Expand Down Expand Up @@ -157,6 +174,7 @@ public static CertificationData fromTransaction(Transaction transaction, byte[]
transaction.getLockScript(),
transaction.getSourceStateHash(),
transaction.calculateTransactionHash(),
transaction.getExpiresAt().orElse(null),
unlockScript
);
}
Expand All @@ -170,12 +188,12 @@ public byte[] toCbor() {
return CborSerializer.encodeTag(
CertificationData.CBOR_TAG,
CborSerializer.encodeArray(
CborSerializer.encodeUnsignedInteger(CertificationData.VERSION),
CborSerializer.encodeUnsignedInteger(VERSION),
this.lockScript.toCbor(),
CborSerializer.encodeByteString(this.sourceStateHash.getData()),
CborSerializer.encodeByteString(this.transactionHash.getData()),
CborSerializer.encodeByteString(this.unlockScript)
)
CborSerializer.encodeNullable(this.expiresAt, CborSerializer::encodeUnsignedInteger),
CborSerializer.encodeByteString(this.unlockScript))
);
}

Expand All @@ -188,19 +206,21 @@ public boolean equals(Object o) {
return Objects.equals(this.lockScript, that.lockScript)
&& Objects.equals(this.sourceStateHash, that.sourceStateHash)
&& Objects.equals(this.transactionHash, that.transactionHash)
&& Objects.equals(this.expiresAt, that.expiresAt)
&& Arrays.equals(this.unlockScript, that.unlockScript);
}

@Override
public int hashCode() {
return Objects.hash(this.lockScript, this.sourceStateHash, this.transactionHash, Arrays.hashCode(this.unlockScript));
return Objects.hash(this.lockScript, this.sourceStateHash, this.transactionHash, this.expiresAt,
Arrays.hashCode(this.unlockScript));
}

@Override
public String toString() {
return String.format(
"CertificationData{lockScript=%s, sourceStateHash=%s, transactionHash=%s, unlockScript=%s}",
this.lockScript, this.sourceStateHash, this.transactionHash,
"CertificationData{lockScript=%s, sourceStateHash=%s, transactionHash=%s, expiresAt=%s, unlockScript=%s}",
this.lockScript, this.sourceStateHash, this.transactionHash, this.expiresAt,
HexConverter.encode(this.unlockScript));
}
}
13 changes: 12 additions & 1 deletion src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ public final class CertificationStatus {
* The certification request failed because request was sent to invalid shard.
*/
public static final CertificationStatus INVALID_SHARD = new CertificationStatus("INVALID_SHARD");
/**
* The round's reference time had already reached the request's timeout.
*/
public static final CertificationStatus REQUEST_EXPIRED =
new CertificationStatus("REQUEST_EXPIRED");

/** The aggregation service has not obtained a consensus reference time yet. */
public static final CertificationStatus SERVICE_NOT_READY =
new CertificationStatus("SERVICE_NOT_READY");

private static final CertificationStatus[] VALUES = {
SUCCESS,
Expand All @@ -64,7 +73,9 @@ public final class CertificationStatus {
INVALID_SOURCE_STATE_HASH_FORMAT,
INVALID_TRANSACTION_HASH_FORMAT,
UNSUPPORTED_ALGORITHM,
INVALID_SHARD
INVALID_SHARD,
REQUEST_EXPIRED,
SERVICE_NOT_READY
};

private final String value;
Expand Down
83 changes: 58 additions & 25 deletions src/main/java/org/unicitylabs/sdk/api/InclusionProof.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,36 @@
*/
public class InclusionProof {
public static final long CBOR_TAG = 39033;
private static final int VERSION = 1;
static final int VERSION = 1;

private final InclusionCertificate inclusionCertificate;
private final CertificationData certificationData;
private final long referenceTime;
private final UnicityCertificate unicityCertificate;

/**
* An InclusionProof describes a certified leaf, so every field is present. The aggregator's
* answer for a state it has not certified yet is not an InclusionProof at all — see
* {@link InclusionProofResponse}, which is the type that can express it.
*/
InclusionProof(
CertificationData certificationData,
long referenceTime,
InclusionCertificate inclusionCertificate,
UnicityCertificate unicityCertificate
) {
Objects.requireNonNull(certificationData, "Certification data cannot be null.");
Objects.requireNonNull(inclusionCertificate, "Inclusion certificate cannot be null.");
Objects.requireNonNull(unicityCertificate, "Unicity certificate cannot be null.");

this.inclusionCertificate = inclusionCertificate;
this.certificationData = certificationData;
this.referenceTime = referenceTime;
this.unicityCertificate = unicityCertificate;
}

public int getVersion() {
return InclusionProof.VERSION;
return VERSION;
}

/**
Expand All @@ -55,12 +65,25 @@ public UnicityCertificate getUnicityCertificate() {
}

/**
* Get certification data on inclusion proof, null on non inclusion proof.
* Get certification data of the certified leaf.
*
* @return certification data
*/
public CertificationData getCertificationData() {
return this.certificationData;
}

/**
* Get the reference time of the round the certified leaf was created in, in Unix seconds.
*
* <p>It cannot be recovered from the certificate chain: an aggregator serves proofs against the
* current certified root, whose input record time is that of the latest round rather than the
* one the leaf was created under.
*
* @return authenticator
* @return reference time
*/
public Optional<CertificationData> getCertificationData() {
return Optional.ofNullable(this.certificationData);
public long getReferenceTime() {
return this.referenceTime;
}

/**
Expand All @@ -74,19 +97,30 @@ public static InclusionProof fromCbor(byte[] bytes) {
if (tag.getTag() != InclusionProof.CBOR_TAG) {
throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag()));
}
List<byte[]> data = CborDeserializer.decodeArray(tag.getData(), 4);
List<byte[]> data = CborDeserializer.decodeArray(tag.getData(), 5);

int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt();
if (version != InclusionProof.VERSION) {
if (version != VERSION) {
throw new CborSerializationException(String.format("Unsupported version: %s", version));
}

CertificationData certificationData =
CborDeserializer.decodeNullable(data.get(1), CertificationData::fromCbor);
Long referenceTime = CborDeserializer.decodeNullable(data.get(2), value ->
CborDeserializer.decodeUnsignedInteger(value).asLong());
InclusionCertificate inclusionCertificate =
CborDeserializer.decodeNullable(data.get(3), (certificate) ->
InclusionCertificate.decode(CborDeserializer.decodeByteString(certificate)));
if (certificationData == null || referenceTime == null || inclusionCertificate == null) {
throw new CborSerializationException(
"Expected a certified leaf, but the inclusion proof describes none.");
}

return new InclusionProof(
CborDeserializer.decodeNullable(data.get(1), CertificationData::fromCbor),
CborDeserializer.decodeNullable(data.get(2), (inclusionCertificate) ->
InclusionCertificate.decode(CborDeserializer.decodeByteString(inclusionCertificate))
),
UnicityCertificate.fromCbor(data.get(3))
certificationData,
referenceTime,
inclusionCertificate,
UnicityCertificate.fromCbor(data.get(4))
);
}

Expand All @@ -96,16 +130,14 @@ public static InclusionProof fromCbor(byte[] bytes) {
* @return CBOR bytes
*/
public byte[] toCbor() {
byte[] payload = CborSerializer.encodeArray(CborSerializer.encodeUnsignedInteger(VERSION),
this.certificationData.toCbor(),
CborSerializer.encodeUnsignedInteger(this.referenceTime),
CborSerializer.encodeByteString(this.inclusionCertificate.encode()),
this.unicityCertificate.toCbor());
return CborSerializer.encodeTag(
InclusionProof.CBOR_TAG,
CborSerializer.encodeArray(
CborSerializer.encodeUnsignedInteger(InclusionProof.VERSION),
CborSerializer.encodeNullable(this.certificationData, CertificationData::toCbor),
CborSerializer.encodeNullable(this.inclusionCertificate, (inclusionCertificate) ->
CborSerializer.encodeByteString(inclusionCertificate.encode())
),
this.unicityCertificate.toCbor()
)
payload
);
}

Expand All @@ -115,20 +147,21 @@ public boolean equals(Object o) {
return false;
}
InclusionProof that = (InclusionProof) o;
return Objects.equals(this.inclusionCertificate, that.inclusionCertificate) && Objects.equals(this.certificationData, that.certificationData) && Objects.equals(this.unicityCertificate, that.unicityCertificate);
return Objects.equals(this.inclusionCertificate, that.inclusionCertificate) && Objects.equals(this.certificationData, that.certificationData) && Objects.equals(this.referenceTime, that.referenceTime) && Objects.equals(this.unicityCertificate, that.unicityCertificate);
}

@Override
public int hashCode() {
return Objects.hash(this.inclusionCertificate, this.certificationData, this.unicityCertificate);
return Objects.hash(this.inclusionCertificate, this.certificationData, this.referenceTime, this.unicityCertificate);
}

@Override
public String toString() {
return String.format(
"InclusionProof{certificationData=%s, inclusionCertificate=%s, unicityCertificate=%s}",
this.inclusionCertificate,
"InclusionProof{certificationData=%s, referenceTime=%s, inclusionCertificate=%s, unicityCertificate=%s}",
this.certificationData,
this.referenceTime,
this.inclusionCertificate,
this.unicityCertificate
);
}
Expand Down
Loading
Loading