Repository: hyperledger/quilt
Pull request #461 (2020-05-09 16:40:26)

Add typeData field to InterledgerPacket
Allows any InterledgerPacket to contain a typed variant of the packet's `data` payload. This is specifically added to optimize STREAM packet handling so that different parts of the Connector or ILP node runtime do not need to re-decrypt STREAM information if a packet is operated upon at various parts of the PacketSwitch.

The field is optional so it can be added in a backwards-compatible manner (and also this contract makes the most sense because typedData may not exist in all use-cases).

Signed-off-by: David Fuelling <sappenin@gmail.com>


Patch:
diff --git a/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java b/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java
index 2bf1b80..4d1f425 100644
--- a/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java
+++ b/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java
@@ -132,6 +132,7 @@ public class IlpPacketEmitter {
     return InterledgerFulfillPacket.builder()
         .fulfillment(InterledgerFulfillment.of(new byte[32]))
         .data(streamPacketData)
+        .typedData(streamPacket)
         .build();
   }
 
@@ -172,7 +173,8 @@ public class IlpPacketEmitter {
         .getCondition();
     return preparePacketBuilder()
         .executionCondition(executionCondition)
-        .data(streamPacketData);
+        .data(streamPacketData)
+        .typedData(streamPacket);
   }
 
   private static InterledgerRejectPacket rejectPacketWithStreamFrames() throws IOException {
diff --git a/ildcp-core/src/main/java/org/interledger/ildcp/IldcpRequestPacket.java b/ildcp-core/src/main/java/org/interledger/ildcp/IldcpRequestPacket.java
index 93004bd..20d6bc8 100644
--- a/ildcp-core/src/main/java/org/interledger/ildcp/IldcpRequestPacket.java
+++ b/ildcp-core/src/main/java/org/interledger/ildcp/IldcpRequestPacket.java
@@ -32,6 +32,7 @@ import org.immutables.value.Value.Default;
 import org.immutables.value.Value.Derived;
 
 import java.time.Instant;
+import java.util.Optional;
 
 /**
  * An extension of {@link InterledgerPreparePacket} that can be used as an IL-DCP request over Interledger.
@@ -62,9 +63,8 @@ public interface IldcpRequestPacket extends InterledgerPreparePacket {
   }
 
   /**
-   * The execution_condition of an ILP packet for IL-DCP is always
-   * {@code Zmh6rfhivXdsj8GLjp+OIAiXFIVu4jOzkCpZHQ1fKSU=} in Base64 format, which is the SHA-256 hash of a 32-byte
-   * array with all 0 values.
+   * The execution_condition of an ILP packet for IL-DCP is always {@code Zmh6rfhivXdsj8GLjp+OIAiXFIVu4jOzkCpZHQ1fKSU=}
+   * in Base64 format, which is the SHA-256 hash of a 32-byte array with all 0 values.
    */
   @Override
   default InterledgerCondition getExecutionCondition() {
@@ -98,6 +98,11 @@ public interface IldcpRequestPacket extends InterledgerPreparePacket {
     return EMPTY_DATA;
   }
 
+  @Override
+  default Optional<Object> typedData() {
+    return Optional.empty();
+  }
+
   /**
    * Exists to satisfy Immutables.
    */
diff --git a/ildcp-core/src/main/java/org/interledger/ildcp/IldcpResponsePacket.java b/ildcp-core/src/main/java/org/interledger/ildcp/IldcpResponsePacket.java
index 619ec39..2757023 100644
--- a/ildcp-core/src/main/java/org/interledger/ildcp/IldcpResponsePacket.java
+++ b/ildcp-core/src/main/java/org/interledger/ildcp/IldcpResponsePacket.java
@@ -9,9 +9,9 @@ package org.interledger.ildcp;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -26,6 +26,8 @@ import org.interledger.core.InterledgerFulfillment;
 
 import org.immutables.value.Value.Default;
 
+import java.util.Optional;
+
 /**
  * An extension of {@link InterledgerFulfillPacket} that is also a {@link IldcpResponsePacket} that can be used as an
  * IL-DCP response over Interledger.
@@ -52,6 +54,11 @@ public interface IldcpResponsePacket extends InterledgerFulfillPacket {
    */
   IldcpResponse getIldcpResponse();
 
+  @Override
+  default Optional<Object> typedData() {
+    return Optional.empty();
+  }
+
   /**
    * Exists to satisfy Immutables.
    */
diff --git a/ilp-core/src/main/java/org/interledger/core/InterledgerPacket.java b/ilp-core/src/main/java/org/interledger/core/InterledgerPacket.java
index a82d7a4..c57ccee 100644
--- a/ilp-core/src/main/java/org/interledger/core/InterledgerPacket.java
+++ b/ilp-core/src/main/java/org/interledger/core/InterledgerPacket.java
@@ -20,6 +20,8 @@ package org.interledger.core;
  * =========================LICENSE_END==================================
  */
 
+import java.util.Optional;
+
 public interface InterledgerPacket {
 
   /**
@@ -29,4 +31,12 @@ public interface InterledgerPacket {
    * @return A byte array.
    */
   byte[] getData();
+
+  /**
+   * Typed variant of {@link #getData}. This method exists so that implementations can constructed object (e.g., a
+   * StreamPacket) and use them in various places in code without having to decode this object's bytes more than once.
+   *
+   * @return An optionally-present {@link Object} that can be cast to an appropriate type.
+   */
+  Optional<Object> typedData();
 }
diff --git a/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java b/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java
index 2a273d1..ee02188 100644
--- a/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java
+++ b/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java
@@ -494,6 +494,7 @@ public class SimpleStreamSender implements StreamSender {
           .executionCondition(executionCondition)
           .expiresAt(DateUtils.now().plusSeconds(30L))
           .data(streamPacketData)
+          .typedData(streamPacket)
           .build();
 
       // The function that parses out the STREAM packets...
@@ -604,6 +605,8 @@ public class SimpleStreamSender implements StreamSender {
             .executionCondition(executionCondition)
             .expiresAt(DateUtils.now().plusSeconds(30L))
             .data(streamPacketData)
+            // Added here for JVM convenience, but only the bytes above are encoded to ASN.1 OER
+            .typedData(streamPacket)
             .build();
 
         // auth
diff --git a/stream-parent/stream-receiver/src/main/java/org/interledger/stream/receiver/StatelessStreamReceiver.java b/stream-parent/stream-receiver/src/main/java/org/interledger/stream/receiver/StatelessStreamReceiver.java
index 10da2f2..5a9a1c8 100644
--- a/stream-parent/stream-receiver/src/main/java/org/interledger/stream/receiver/StatelessStreamReceiver.java
+++ b/stream-parent/stream-receiver/src/main/java/org/interledger/stream/receiver/StatelessStreamReceiver.java
@@ -173,6 +173,7 @@ public class StatelessStreamReceiver implements StreamReceiver {
         return InterledgerFulfillPacket.builder()
             .fulfillment(fulfillment)
             .data(encryptedReturnableStreamPacketBytes)
+            .typedData(returnableStreamPacketResponse)
             .build();
 
       } catch (IOException e) {
@@ -212,6 +213,7 @@ public class StatelessStreamReceiver implements StreamReceiver {
             .message("STREAM packet not fulfillable (prepare amount < stream packet amount)")
             .triggeredBy(receiverAddress)
             .data(encryptedReturnableStreamPacketBytes)
+            .typedData(returnableStreamPacketResponse)
             .build();
       } catch (IOException e) {
         throw new StreamException(e.getMessage(), e);


Test patch:
diff --git a/stream-parent/stream-receiver/src/test/java/org/interledger/stream/receiver/StatelessStreamReceiverTest.java b/stream-parent/stream-receiver/src/test/java/org/interledger/stream/receiver/StatelessStreamReceiverTest.java
index f8be1cc..267a4f9 100644
--- a/stream-parent/stream-receiver/src/test/java/org/interledger/stream/receiver/StatelessStreamReceiverTest.java
+++ b/stream-parent/stream-receiver/src/test/java/org/interledger/stream/receiver/StatelessStreamReceiverTest.java
@@ -13,6 +13,7 @@ import org.interledger.core.InterledgerCondition;
 import org.interledger.core.InterledgerErrorCode;
 import org.interledger.core.InterledgerPacketType;
 import org.interledger.core.InterledgerPreparePacket;
+import org.interledger.core.InterledgerPreparePacketBuilder;
 import org.interledger.core.InterledgerRejectPacket;
 import org.interledger.core.SharedSecret;
 import org.interledger.encoding.asn.framework.CodecContext;
@@ -30,6 +31,7 @@ import org.interledger.stream.frames.StreamFrameType;
 import org.interledger.stream.frames.StreamMoneyFrame;
 
 import com.google.common.primitives.UnsignedLong;
+import org.immutables.value.Value.Immutable;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -44,6 +46,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.time.Instant;
 import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Unit tests for {@link StatelessStreamReceiver}.
@@ -148,41 +151,25 @@ public class StatelessStreamReceiverTest {
   }
 
   @Test
-  public void testFulfillsValidPacket() throws IOException {
-    streamReceiver = new StatelessStreamReceiver(
-        serverSecretSupplier, streamConnectionGenerator, streamEncryptionService, StreamCodecContextFactory.oer()
-    );
-    final InterledgerAddress receiverAddress = InterledgerAddress.of("example.receiver");
-
-    final StreamConnectionDetails connectionDetails = streamConnectionGenerator
-        .generateConnectionDetails(serverSecretSupplier, receiverAddress);
-    final SharedSecret sharedSecret = SharedSecret.of(connectionDetails.sharedSecret().key());
+  public void testFulfillsValidPacketWithTypedData() throws IOException {
+    final ValidPrepareDetails details = constructValidPreparePacket();
+    final InterledgerPreparePacket preparePacket = details.preparePacketBuilder().build();
+    final InterledgerAddress receiverAddress = details.receiverAddress();
+    final InterledgerCondition executionCondition = details.executionCondition();
 
-    final StreamPacket testStreamPacket = StreamPacket.builder()
-        .interledgerPacketType(InterledgerPacketType.PREPARE)
-        .prepareAmount(UnsignedLong.ZERO)
-        .sequence(UnsignedLong.ONE)
-        .addFrames(StreamMoneyFrame.builder()
-            .streamId(UnsignedLong.ONE)
-            .shares(UnsignedLong.ONE)
-            .build())
-        .build();
-
-    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    streamCodecContext.write(testStreamPacket, baos);
-    final byte[] encryptedStreamPacketBytes = streamEncryptionService.encrypt(sharedSecret, baos.toByteArray());
-
-    final InterledgerCondition executionCondition = StreamUtils
-        .generatedFulfillableFulfillment(sharedSecret, encryptedStreamPacketBytes)
-        .getCondition();
+    this.streamReceiver.receiveMoney(preparePacket, receiverAddress, DENOMINATION).handle(
+        fulfillPacket -> assertThat(fulfillPacket.getFulfillment().getCondition()).isEqualTo(executionCondition),
+        rejectPacket -> {
+          throw new RuntimeException("should have fulfilled");
+        });
+  }
 
-    final InterledgerPreparePacket preparePacket = InterledgerPreparePacket.builder()
-        .destination(connectionDetails.destinationAddress())
-        .amount(UnsignedLong.valueOf(100L))
-        .expiresAt(Instant.EPOCH)
-        .data(encryptedStreamPacketBytes)
-        .executionCondition(executionCondition)
-        .build();
+  @Test
+  public void testFulfillsValidPacketWithNoTypedData() throws IOException {
+    final ValidPrepareDetails details = constructValidPreparePacket();
+    final InterledgerPreparePacket preparePacket = details.preparePacketBuilder().typedData(Optional.empty()).build();
+    final InterledgerAddress receiverAddress = details.receiverAddress();
+    final InterledgerCondition executionCondition = details.executionCondition();
 
     this.streamReceiver.receiveMoney(preparePacket, receiverAddress, DENOMINATION).handle(
         fulfillPacket -> assertThat(fulfillPacket.getFulfillment().getCondition()).isEqualTo(executionCondition),
@@ -230,6 +217,7 @@ public class StatelessStreamReceiverTest {
         .amount(UnsignedLong.valueOf(100L))
         .expiresAt(Instant.EPOCH)
         .data(encryptedStreamPacketBytes)
+        .typedData(testStreamPacket)
         .executionCondition(executionCondition)
         .build();
 
@@ -266,14 +254,20 @@ public class StatelessStreamReceiverTest {
         .triggeredBy(CLIENT_ADDRESS)
         .build();
 
+    final AtomicBoolean rejectHandled = new AtomicBoolean();
     this.streamReceiver.receiveMoney(createPreparePacket(
         InterledgerCondition.of(new byte[32])), CLIENT_ADDRESS, DENOMINATION
     ).handle(
         fulfillPacket -> {
           throw new RuntimeException("should have rejected");
         },
-        rejectPacket -> assertThat(rejectPacket).isEqualTo(expected)
+        rejectPacket -> {
+          rejectHandled.set(true);
+          assertThat(rejectPacket).isEqualTo(expected);
+        }
     );
+
+    assertThat(rejectHandled).isTrue();
   }
 
   @Test
@@ -347,31 +341,31 @@ public class StatelessStreamReceiverTest {
   }
 
   @Test
-  public void receiveMoneyWithEmptyStreamPacketRejects() throws Exception {
+  public void receiveMoneyWithEmptyStreamPacketRejects() {
     this.connectionDetails = streamConnectionGenerator.generateConnectionDetails(serverSecretSupplier, CLIENT_ADDRESS);
 
     streamReceiver = new StatelessStreamReceiver(
-      serverSecretSupplier, streamConnectionGenerator, streamEncryptionService, StreamCodecContextFactory.oer()
+        serverSecretSupplier, streamConnectionGenerator, streamEncryptionService, StreamCodecContextFactory.oer()
     );
 
     InterledgerPreparePacket prepare = InterledgerPreparePacket.builder()
-      .destination(connectionDetails.destinationAddress())
-      .amount(UnsignedLong.valueOf(100L))
-      .expiresAt(Instant.EPOCH)
-      .data(new byte[0])
-      .executionCondition(InterledgerCondition.of(new byte[32]))
-      .build();
+        .destination(connectionDetails.destinationAddress())
+        .amount(UnsignedLong.valueOf(100L))
+        .expiresAt(Instant.EPOCH)
+        .data(new byte[0])
+        .executionCondition(InterledgerCondition.of(new byte[32]))
+        .build();
 
     this.streamReceiver.receiveMoney(prepare, CLIENT_ADDRESS, DENOMINATION).handle(
-      fulfillPacket -> {
-        throw new RuntimeException("should have rejected");
-      },
-      rejectPacket -> assertThat(rejectPacket).extracting("code", "message", "triggeredBy")
-        .containsExactly(
-          InterledgerErrorCode.F06_UNEXPECTED_PAYMENT,
-          "No STREAM packet bytes available to decrypt",
-          Optional.of(CLIENT_ADDRESS)
-        ));
+        fulfillPacket -> {
+          throw new RuntimeException("should have rejected");
+        },
+        rejectPacket -> assertThat(rejectPacket).extracting("code", "message", "triggeredBy")
+            .containsExactly(
+                InterledgerErrorCode.F06_UNEXPECTED_PAYMENT,
+                "No STREAM packet bytes available to decrypt",
+                Optional.of(CLIENT_ADDRESS)
+            ));
   }
 
   @Test
@@ -409,6 +403,7 @@ public class StatelessStreamReceiverTest {
         .amount(UnsignedLong.valueOf(100L))
         .expiresAt(Instant.EPOCH)
         .data(encryptedStreamPacketBytes)
+        .typedData(testStreamPacket)
         .executionCondition(executionCondition)
         .build();
 
@@ -452,6 +447,7 @@ public class StatelessStreamReceiverTest {
         .amount(UnsignedLong.valueOf(100L))
         .expiresAt(Instant.EPOCH)
         .data(encryptedStreamPacketBytes)
+        .typedData(testStreamPacket)
         .executionCondition(executionCondition)
         .build();
   }
@@ -462,5 +458,55 @@ public class StatelessStreamReceiverTest {
     return streamEncryptionService.encrypt(SharedSecret.of(connectionDetails.sharedSecret().key()), baos.toByteArray());
   }
 
+  private ValidPrepareDetails constructValidPreparePacket() throws IOException {
+    streamReceiver = new StatelessStreamReceiver(
+        serverSecretSupplier, streamConnectionGenerator, streamEncryptionService, StreamCodecContextFactory.oer()
+    );
+    final InterledgerAddress receiverAddress = InterledgerAddress.of("example.receiver");
+
+    final StreamConnectionDetails connectionDetails = streamConnectionGenerator
+        .generateConnectionDetails(serverSecretSupplier, receiverAddress);
+    final SharedSecret sharedSecret = SharedSecret.of(connectionDetails.sharedSecret().key());
 
+    final StreamPacket testStreamPacket = StreamPacket.builder()
+        .interledgerPacketType(InterledgerPacketType.PREPARE)
+        .prepareAmount(UnsignedLong.ZERO)
+        .sequence(UnsignedLong.ONE)
+        .addFrames(StreamMoneyFrame.builder()
+            .streamId(UnsignedLong.ONE)
+            .shares(UnsignedLong.ONE)
+            .build())
+        .build();
+
+    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    streamCodecContext.write(testStreamPacket, baos);
+    final byte[] encryptedStreamPacketBytes = streamEncryptionService.encrypt(sharedSecret, baos.toByteArray());
+
+    final InterledgerCondition executionCondition = StreamUtils
+        .generatedFulfillableFulfillment(sharedSecret, encryptedStreamPacketBytes)
+        .getCondition();
+
+    return ImmutableValidPrepareDetails.builder()
+        .preparePacketBuilder(InterledgerPreparePacket.builder()
+            .destination(connectionDetails.destinationAddress())
+            .amount(UnsignedLong.valueOf(100L))
+            .expiresAt(Instant.EPOCH)
+            .data(encryptedStreamPacketBytes)
+            .typedData(testStreamPacket)
+            .executionCondition(executionCondition)
+        )
+        .receiverAddress(receiverAddress)
+        .executionCondition(executionCondition)
+        .build();
+  }
+
+  @Immutable
+  public interface ValidPrepareDetails {
+
+    InterledgerPreparePacketBuilder preparePacketBuilder();
+
+    InterledgerAddress receiverAddress();
+
+    InterledgerCondition executionCondition();
+  }
 }