Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/
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
56 changes: 26 additions & 30 deletions src/main/java/org/unicitylabs/sdk/api/InclusionProof.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,32 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

/**
* Represents a proof of inclusion or non-inclusion in a sparse merkle tree.
*/
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 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,
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;
Expand Down Expand Up @@ -59,26 +65,25 @@ public UnicityCertificate getUnicityCertificate() {
}

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

/**
* Get the reference time of the round the certified leaf was created in, empty on a
* non-inclusion proof.
* 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.
* <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 reference time
*/
public Optional<Long> getReferenceTime() {
return Optional.ofNullable(this.referenceTime);
public long getReferenceTime() {
return this.referenceTime;
}

/**
Expand Down Expand Up @@ -106,17 +111,9 @@ public static InclusionProof fromCbor(byte[] bytes) {
InclusionCertificate inclusionCertificate =
CborDeserializer.decodeNullable(data.get(3), (certificate) ->
InclusionCertificate.decode(CborDeserializer.decodeByteString(certificate)));

// A proof either establishes a leaf or reports that there is none yet. A partially present
// proof is neither, and would let a caller reach a leaf check with a reference time nothing
// certified.
long present = Stream.of(certificationData, referenceTime, inclusionCertificate)
.filter(Objects::nonNull)
.count();
if (present != 0 && present != 3) {
if (certificationData == null || referenceTime == null || inclusionCertificate == null) {
throw new CborSerializationException(
"InclusionProof must carry certification data, reference time and inclusion "
+ "certificate together, or none of them.");
"Expected a certified leaf, but the inclusion proof describes none.");
}

return new InclusionProof(
Expand All @@ -134,11 +131,10 @@ public static InclusionProof fromCbor(byte[] bytes) {
*/
public byte[] toCbor() {
byte[] payload = CborSerializer.encodeArray(CborSerializer.encodeUnsignedInteger(VERSION),
CborSerializer.encodeNullable(this.certificationData, CertificationData::toCbor),
CborSerializer.encodeNullable(this.referenceTime,
CborSerializer::encodeUnsignedInteger),
CborSerializer.encodeNullable(this.inclusionCertificate, certificate ->
CborSerializer.encodeByteString(certificate.encode())), this.unicityCertificate.toCbor());
this.certificationData.toCbor(),
CborSerializer.encodeUnsignedInteger(this.referenceTime),
CborSerializer.encodeByteString(this.inclusionCertificate.encode()),
this.unicityCertificate.toCbor());
return CborSerializer.encodeTag(
InclusionProof.CBOR_TAG,
payload
Expand Down
125 changes: 110 additions & 15 deletions src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,78 @@
package org.unicitylabs.sdk.api;

import org.unicitylabs.sdk.api.bft.UnicityCertificate;
import org.unicitylabs.sdk.serializer.cbor.CborDeserializer;
import org.unicitylabs.sdk.serializer.cbor.CborSerializationException;
import org.unicitylabs.sdk.serializer.cbor.CborSerializer;

import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

/**
* Inclusion proof response.
* What the aggregator answers when asked about a state.
*
* <p>This is the wire shape, and it has two forms: a certified leaf, or the absence of one.
* Keeping that distinction here rather than inside {@link InclusionProof} is what lets the proof
* itself be complete by construction — a verifier holding one never has to ask whether it
* describes a leaf.
*/
public class InclusionProofResponse {

private final long blockNumber;
private final InclusionProof inclusionProof;
private final UnicityCertificate unicityCertificate;

/**
* Create inclison proof response.
*
* @param inclusionProof inclusion proof
*/
InclusionProofResponse(
private InclusionProofResponse(
long blockNumber,
InclusionProof inclusionProof
InclusionProof inclusionProof,
UnicityCertificate unicityCertificate
) {
this.blockNumber = blockNumber;
this.inclusionProof = inclusionProof;
this.unicityCertificate = unicityCertificate;
}

/**
* Get inclusion proof.
* The aggregator has certified this state.
*
* <p>The round it was served against is the proof's own, so there is no second certificate to
* supply and none that could disagree with it.
*
* @return inclusion proof
* @param blockNumber block number the answer was served at
* @param inclusionProof the certified leaf
* @return the response
*/
public static InclusionProofResponse certified(long blockNumber, InclusionProof inclusionProof) {
return new InclusionProofResponse(blockNumber, inclusionProof,
inclusionProof.getUnicityCertificate());
}

/**
* The aggregator has not certified this state yet, so only the round is meaningful.
*
* @param blockNumber block number the answer was served at
* @param unicityCertificate certificate of the round the answer was served against
* @return the response
*/
public static InclusionProofResponse notCertified(long blockNumber,
UnicityCertificate unicityCertificate) {
return new InclusionProofResponse(blockNumber, null, unicityCertificate);
}

/**
* Get the certificate of the round this answer was served against. Present either way.
*
* @return unicity certificate
*/
public UnicityCertificate getUnicityCertificate() {
return this.unicityCertificate;
}

/**
* Get the certified leaf, or null when the state is not certified yet.
*
* @return inclusion proof, or null
*/
public InclusionProof getInclusionProof() {
return this.inclusionProof;
Expand All @@ -43,10 +86,44 @@ public InclusionProof getInclusionProof() {
*/
public static InclusionProofResponse fromCbor(byte[] bytes) {
List<byte[]> data = CborDeserializer.decodeArray(bytes, 2);
return new InclusionProofResponse(
CborDeserializer.decodeUnsignedInteger(data.get(0)).asLong(),
InclusionProof.fromCbor(data.get(1))
);
long blockNumber = CborDeserializer.decodeUnsignedInteger(data.get(0)).asLong();

CborDeserializer.CborTag tag = CborDeserializer.decodeTag(data.get(1));
if (tag.getTag() != InclusionProof.CBOR_TAG) {
throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag()));
}
List<byte[]> proof = CborDeserializer.decodeArray(tag.getData(), 5);
int version = CborDeserializer.decodeUnsignedInteger(proof.get(0)).asInt();
if (version != InclusionProof.VERSION) {
throw new CborSerializationException(String.format("Unsupported version: %s", version));
}

CertificationData certificationData =
CborDeserializer.decodeNullable(proof.get(1), CertificationData::fromCbor);
Long referenceTime = CborDeserializer.decodeNullable(proof.get(2), value ->
CborDeserializer.decodeUnsignedInteger(value).asLong());
InclusionCertificate inclusionCertificate =
CborDeserializer.decodeNullable(proof.get(3), (certificate) ->
InclusionCertificate.decode(CborDeserializer.decodeByteString(certificate)));
UnicityCertificate unicityCertificate = UnicityCertificate.fromCbor(proof.get(4));

// The three leaf fields travel together: all present once the request has been included in a
// certified round, all absent while it is still pending. Anything in between is a protocol
// violation, and rejecting it here is what lets InclusionProof require all three.
long present = Stream.of(certificationData, referenceTime, inclusionCertificate)
.filter(Objects::nonNull)
.count();
if (present == 0) {
return InclusionProofResponse.notCertified(blockNumber, unicityCertificate);
}
if (present != 3) {
throw new CborSerializationException(
"InclusionProof must carry certification data, reference time and inclusion "
+ "certificate together, or none of them.");
}

return InclusionProofResponse.certified(blockNumber, new InclusionProof(certificationData,
referenceTime, inclusionCertificate, unicityCertificate));
}

/**
Expand All @@ -57,8 +134,26 @@ public static InclusionProofResponse fromCbor(byte[] bytes) {
public byte[] toCbor() {
return CborSerializer.encodeArray(
CborSerializer.encodeUnsignedInteger(this.blockNumber),
this.inclusionProof.toCbor()
this.inclusionProof == null
? this.encodeNoCertifiedLeaf()
: this.inclusionProof.toCbor()
);
}

/**
* Encode the wire form for a state with no certified leaf: the three leaf fields absent, the
* round's certificate still present.
*
* @return CBOR bytes
*/
private byte[] encodeNoCertifiedLeaf() {
return CborSerializer.encodeTag(
InclusionProof.CBOR_TAG,
CborSerializer.encodeArray(
CborSerializer.encodeUnsignedInteger(InclusionProof.VERSION),
CborSerializer.encodeNull(),
CborSerializer.encodeNull(),
CborSerializer.encodeNull(),
this.unicityCertificate.toCbor()));
}
}
Loading
Loading