All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.interledger.stream.sender.SimpleStreamSender Maven / Gradle / Ivy

There is a newer version: 1.3.1
Show newest version
package org.interledger.stream.sender;

import static org.interledger.core.InterledgerErrorCode.F08_AMOUNT_TOO_LARGE;
import static org.interledger.core.InterledgerErrorCode.F99_APPLICATION_ERROR;
import static org.interledger.stream.StreamUtils.generatedFulfillableFulfillment;

import org.interledger.codecs.stream.StreamCodecContextFactory;
import org.interledger.core.DateUtils;
import org.interledger.core.Immutable;
import org.interledger.core.InterledgerAddress;
import org.interledger.core.InterledgerCondition;
import org.interledger.core.InterledgerErrorCode;
import org.interledger.core.InterledgerErrorCode.ErrorFamily;
import org.interledger.core.InterledgerFulfillPacket;
import org.interledger.core.InterledgerPacketType;
import org.interledger.core.InterledgerPreparePacket;
import org.interledger.core.InterledgerRejectPacket;
import org.interledger.core.InterledgerResponsePacket;
import org.interledger.core.SharedSecret;
import org.interledger.encoding.asn.framework.CodecContext;
import org.interledger.link.Link;
import org.interledger.stream.Denomination;
import org.interledger.stream.PaymentTracker;
import org.interledger.stream.PrepareAmounts;
import org.interledger.stream.SendMoneyRequest;
import org.interledger.stream.SendMoneyResult;
import org.interledger.stream.StreamConnection;
import org.interledger.stream.StreamConnectionClosedException;
import org.interledger.stream.StreamConnectionId;
import org.interledger.stream.StreamPacket;
import org.interledger.stream.crypto.JavaxStreamEncryptionService;
import org.interledger.stream.crypto.StreamEncryptionService;
import org.interledger.stream.frames.ConnectionAssetDetailsFrame;
import org.interledger.stream.frames.ConnectionNewAddressFrame;
import org.interledger.stream.frames.StreamFrame;
import org.interledger.stream.frames.StreamFrameType;
import org.interledger.stream.frames.StreamMoneyFrame;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.primitives.UnsignedLong;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.immutables.value.Value.Derived;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.annotation.concurrent.ThreadSafe;

/**
 * 

A simple implementation of {@link StreamSender} that opens a STREAM connection, sends money, and then closes the * connection, yielding a response.

* *

Note that this implementation does not currently support sending data, which is defined in the STREAM * protocol.

* *

Note that, per https://github.com/hyperledger/quilt/issues/242, as of the publication of this client, * connectors will reject ILP packets that exceed 32kb. This implementation does not overtly restrict the size of the * data field in any particular {@link InterledgerPreparePacket}, for two reasons. First, this implementation never * packs a sufficient number of STREAM frames into a single Prepare packet for this 32kb limit to ever be reached; * Second, if the ILPv4 RFC ever changes to increase this size limitation, we don't want sender/receiver software to * have to be updated across the Interledger.

*/ @ThreadSafe public class SimpleStreamSender implements StreamSender { private final Link link; private final Duration sendPacketSleepDuration; private final StreamEncryptionService streamEncryptionService; private final StreamConnectionManager streamConnectionManager; private final ExecutorService executorService; /** * Required-args Constructor. * * @param link A {@link Link} that is used to send ILPv4 packets to an immediate peer. */ public SimpleStreamSender(final Link link) { this(link, Duration.ofMillis(10L)); } /** * Required-args Constructor. * * @param link A {@link Link} that is used to send ILPv4 packets to an immediate peer. * @param sendPacketSleepDuration A {@link Duration} representing the amount of time for the soldierOn thread to sleep * before attempting more processing. */ public SimpleStreamSender(final Link link, final Duration sendPacketSleepDuration) { this(link, sendPacketSleepDuration, new JavaxStreamEncryptionService()); } /** * Required-args Constructor. * * @param link A {@link Link} that is used to send ILPv4 packets to an immediate peer. * @param sendPacketSleepDuration A {@link Duration} representing the amount of time for the soldierOn thread to sleep * before attempting more processing. * @param streamEncryptionService An instance of {@link StreamEncryptionService} used to encrypt and decrypted * end-to-end STREAM packet data (i.e., packets that should only be visible between * sender and receiver). */ public SimpleStreamSender( final Link link, final Duration sendPacketSleepDuration, final StreamEncryptionService streamEncryptionService ) { this(link, sendPacketSleepDuration, streamEncryptionService, new StreamConnectionManager()); } /** * Required-args Constructor. * * @param streamEncryptionService An instance of {@link StreamEncryptionService} used to encrypt and decrypted * end-to-end STREAM packet data (i.e., packets that should only be visible between * sender and receiver). * @param link A {@link Link} that is used to send ILPv4 packets to an immediate peer. * @param sendPacketSleepDuration A {@link Duration} representing the amount of time for the soldierOn thread to sleep * before attempting more processing. */ public SimpleStreamSender( final Link link, final Duration sendPacketSleepDuration, final StreamEncryptionService streamEncryptionService, final StreamConnectionManager streamConnectionManager ) { this(link, sendPacketSleepDuration, streamEncryptionService, streamConnectionManager, newDefaultExecutor()); } /** * Required-args Constructor. * * @param link A {@link Link} that is used to send ILPv4 packets to an immediate peer. * @param sendPacketSleepDuration A {@link Duration} representing the amount of time for the soldierOn thread to sleep * before attempting more processing. * @param streamEncryptionService A {@link StreamEncryptionService} used to encrypt and decrypted end-to-end STREAM * packet data (i.e., packets that should only be visible between sender and * receiver). * @param streamConnectionManager A {@link StreamConnectionManager} that manages connections for all senders and * @param executorService A {@link ExecutorService} to run the payments. */ public SimpleStreamSender( final Link link, final Duration sendPacketSleepDuration, final StreamEncryptionService streamEncryptionService, final StreamConnectionManager streamConnectionManager, final ExecutorService executorService ) { this.link = Objects.requireNonNull(link); this.sendPacketSleepDuration = Objects.requireNonNull(sendPacketSleepDuration); this.streamEncryptionService = Objects.requireNonNull(streamEncryptionService); this.streamConnectionManager = Objects.requireNonNull(streamConnectionManager); // Note that pools with similar properties but different details (for example, timeout parameters) may be // created using {@link ThreadPoolExecutor} constructors. this.executorService = Objects.requireNonNull(executorService); } private static ExecutorService newDefaultExecutor() { ThreadFactory factory = new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("simple-stream-sender-%d") .build(); return Executors.newFixedThreadPool(30, factory); } @Override public CompletableFuture sendMoney(final SendMoneyRequest request) { Objects.requireNonNull(request); final StreamConnection streamConnection = this.streamConnectionManager.openConnection( StreamConnectionId.from(request.destinationAddress(), request.sharedSecret()) ); return new SendMoneyAggregator( this.executorService, streamConnection, StreamCodecContextFactory.oer(), this.link, new AimdCongestionController(), this.streamEncryptionService, this.sendPacketSleepDuration, request ).send(); } /** * Contains summary information about a STREAM Connection. */ @Immutable public interface ConnectionStatistics { static ConnectionStatisticsBuilder builder() { return new ConnectionStatisticsBuilder(); } /** * The number of fulfilled packets that were received over the lifetime of this connection. * * @return An int representing the number of fulfilled packets. */ int numFulfilledPackets(); /** * The number of rejected packets that were received over the lifetime of this connection. * * @return An int representing the number of rejected packets. */ int numRejectPackets(); /** * Compute the total number of packets that were fulfilled or rejected on this Connection. * * @return An int representing the total number of response packets processed. */ @Derived default int totalPackets() { return numFulfilledPackets() + numRejectPackets(); } /** * The total amount delivered to the receiver. * * @return An {@link UnsignedLong} representing the total amount delivered. */ UnsignedLong amountDelivered(); } /** * Encapsulates everything needed to send a particular amount of money by breaking up a payment into a bunch of * smaller packets, and then handling all responses. This aggregator operates on a single Connection by opening and * closing a single stream. */ static class SendMoneyAggregator { // These error codes, despite beging FINAL, should be treated as "recoverable", meaning the Stream sendMoney // operation should not immediately fail if one of these is encountered. private static final Set NON_TERMINAL_ERROR_CODES = ImmutableSet.of( F08_AMOUNT_TOO_LARGE, F99_APPLICATION_ERROR ); private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final ExecutorService executorService; private final StreamConnection streamConnection; private final CodecContext streamCodecContext; private final StreamEncryptionService streamEncryptionService; private final CongestionController congestionController; private final Link link; private final SharedSecret sharedSecret; private final Optional timeout; private final InterledgerAddress senderAddress; private final Denomination senderDenomination; private final InterledgerAddress destinationAddress; private final AtomicBoolean shouldSendSourceAddress; private final AtomicInteger numFulfilledPackets; private final AtomicInteger numRejectedPackets; private final PaymentTracker paymentTracker; private final AtomicBoolean unrecoverableErrorEncountered; private final SendMoneyRequest sendMoneyRequest; private Optional receiverDenomination; private Duration sendPacketSleepDuration; /** * Required-args Constructor. * * @param executorService An {@link ExecutorService} for sending multiple STREAM frames in parallel. * @param streamConnection A {@link StreamConnection} that can be used to send packets with. * @param streamCodecContext A {@link CodecContext} that can encode and decode ASN.1 OER Stream packets and * frames. * @param link The {@link Link} used to send ILPv4 packets containing Stream packets. * @param congestionController A {@link CongestionController} that supports back-pressure for money streams. * @param streamEncryptionService A {@link StreamEncryptionService} that allows for Stream packet encryption and * decryption. * @param sendMoneyRequest A {@link SendMoneyRequest} that contains all relevant details about the money to */ SendMoneyAggregator( final ExecutorService executorService, final StreamConnection streamConnection, final CodecContext streamCodecContext, final Link link, final CongestionController congestionController, final StreamEncryptionService streamEncryptionService, final Duration sendPacketSleepDuration, final SendMoneyRequest sendMoneyRequest ) { this.executorService = Objects.requireNonNull(executorService); this.streamConnection = Objects.requireNonNull(streamConnection); this.streamCodecContext = Objects.requireNonNull(streamCodecContext); this.link = Objects.requireNonNull(link); this.streamEncryptionService = Objects.requireNonNull(streamEncryptionService); this.congestionController = Objects.requireNonNull(congestionController); this.shouldSendSourceAddress = new AtomicBoolean(true); this.unrecoverableErrorEncountered = new AtomicBoolean(false); this.sharedSecret = sendMoneyRequest.sharedSecret(); this.senderAddress = sendMoneyRequest.sourceAddress(); this.destinationAddress = sendMoneyRequest.destinationAddress(); this.sendPacketSleepDuration = Objects.requireNonNull(sendPacketSleepDuration); this.numFulfilledPackets = new AtomicInteger(0); this.numRejectedPackets = new AtomicInteger(0); this.sendMoneyRequest = Objects.requireNonNull(sendMoneyRequest); this.timeout = sendMoneyRequest.timeout(); this.senderDenomination = sendMoneyRequest.denomination(); this.paymentTracker = sendMoneyRequest.paymentTracker(); this.receiverDenomination = Optional.empty(); } /** * Send money in an individual stream. * * @return A {@link CompletableFuture} containing a {@link SendMoneyResult}. */ CompletableFuture send() { Objects.requireNonNull(sharedSecret); Objects.requireNonNull(destinationAddress); Instant startPreflight = DateUtils.now(); try { receiverDenomination = preflightCheck(); if (paymentTracker.requiresReceiverDenomination() && !receiverDenomination.isPresent()) { // The PaymentTrack requires a receiver denomination, but the receiver didn't send one. Thus, we must abort. return CompletableFuture.completedFuture(this.constructSendMoneyResultForInvalidPreflight(startPreflight)); } } catch (Exception e) { if (paymentTracker.requiresReceiverDenomination()) { logger.error("Preflight check failed. sendMoneyRequest={}", sendMoneyRequest, e); return CompletableFuture.completedFuture(this.constructSendMoneyResultForInvalidPreflight(startPreflight)); } else { logger.warn( "Preflight check failed, but was not crucial for this sendMoney operation. sendMoneyRequest={}", sendMoneyRequest, e ); } } // A separate executor is needed for overall call to sendMoneyPacketized otherwise a livelock can occur. // Using a shared executor could cause sendMoneyPacketized to internally get blocked from submitting tasks // because the shared executor is already blocked waiting on the results of the call here to sendMoneyPacketized ExecutorService sendMoneyExecutor = Executors.newSingleThreadExecutor(); final Instant start = DateUtils.now(); // All futures will run here using the Cached Executor service. return CompletableFuture .supplyAsync(() -> { // Do all the work of sending packetized money for this Stream/sendMoney request. this.sendMoneyPacketized(); return SendMoneyResult.builder() .amountDelivered(paymentTracker.getDeliveredAmountInReceiverUnits()) .amountSent(paymentTracker.getDeliveredAmountInSenderUnits()) .amountLeftToSend(paymentTracker.getOriginalAmountLeft()) .originalAmount(paymentTracker.getOriginalAmount()) .numFulfilledPackets(numFulfilledPackets.get()) .numRejectPackets(numRejectedPackets.get()) .sendMoneyDuration(Duration.between(start, DateUtils.now())) .successfulPayment(paymentTracker.successful()) .build(); }, sendMoneyExecutor) .whenComplete(($, error) -> { sendMoneyExecutor.shutdown(); if (error != null) { logger.error("SendMoney Stream failed: " + error.getMessage(), error); } if (!$.successfulPayment()) { logger.error("Failed to send full amount. sendMoneyRequestId={}", sendMoneyRequest.requestId()); } }); } /** * Helper method to construct a {@link SendMoneyResult} that can be used when preflight checks are not successful. * * @param startPreflight An {@link Instant} representing the moment in time that preflight was started. * * @return A {@link SendMoneyResult}. */ private SendMoneyResult constructSendMoneyResultForInvalidPreflight(final Instant startPreflight) { Objects.requireNonNull(startPreflight); return SendMoneyResult.builder() .sendMoneyDuration(Duration.between(startPreflight, DateUtils.now())) .numRejectPackets(0) .numFulfilledPackets(0) .amountDelivered(UnsignedLong.ZERO) .amountSent(UnsignedLong.ZERO) .originalAmount(paymentTracker.getOriginalAmount()) .amountLeftToSend(paymentTracker.getOriginalAmountLeft()) .successfulPayment(paymentTracker.successful()) .build(); } /** *

Send a zero-value Prepare packet to "pre-flight" the Connection before actual value is transferred.

* *

This operation is used to initialize a new Stream connection in order to fulfill any prerequisites necessary * before sending real value. For example, it is necessary to obtain the receiver's "Connection Asset Details" * before a sender can send value, in order to manage slippage for the sender.

* *

Likewise, it may be desirable to perform other preflight checks in the future, such as checking an exchange * rate or some other type of check.

* * TODO: See https://github.com/hyperledger/quilt/issues/308 to determine when the Stream and/or Connection should * be closed. * * @return A {@link Denomination} that contains the asset information for the receiver. * * @throws StreamConnectionClosedException if the denomination could not be loaded and the Stream should be closed. */ @VisibleForTesting Optional preflightCheck() throws StreamConnectionClosedException { // Load up the STREAM packet final UnsignedLong sequence; try { sequence = this.streamConnection.nextSequence(); } catch (StreamConnectionClosedException e) { // The Connection is closed, so we can't send anything more on it. logger.error( "Unable to send more packets on a closed StreamConnection. streamConnection={} error={}", streamConnection, e ); throw e; } final List frames = Lists.newArrayList( StreamMoneyFrame.builder() // This aggregator supports only a single stream-id, which is one. .streamId(UnsignedLong.ONE) .shares(UnsignedLong.ONE) .build(), ConnectionNewAddressFrame.builder() .sourceAddress(senderAddress) .build(), ConnectionAssetDetailsFrame.builder() .sourceDenomination(senderDenomination) .build() ); final StreamPacket streamPacket = StreamPacket.builder() .interledgerPacketType(InterledgerPacketType.PREPARE) .prepareAmount(UnsignedLong.ZERO) .sequence(sequence) .frames(frames) .build(); // Create the ILP Prepare packet final byte[] streamPacketData = this.toEncrypted(sharedSecret, streamPacket); final InterledgerCondition executionCondition; executionCondition = generatedFulfillableFulfillment(sharedSecret, streamPacketData).getCondition(); final InterledgerPreparePacket preparePacket = InterledgerPreparePacket.builder() .destination(destinationAddress) .amount(UnsignedLong.ZERO) .executionCondition(executionCondition) .expiresAt(DateUtils.now().plusSeconds(30L)) .data(streamPacketData) .build(); // The function that parses out the STREAM packets... final Function> readDetailsFromStream = (responsePacket) -> { final StreamPacket packet = this.fromEncrypted(sharedSecret, responsePacket.getData()); if (packet != null) { return packet.frames().stream() .filter(f -> f.streamFrameType() == StreamFrameType.ConnectionAssetDetails) .findFirst() .map(f -> (ConnectionAssetDetailsFrame) f) .map(f -> Denomination.builder().from(f.sourceDenomination()).build()); } else { return Optional.empty(); } }; return link.sendPacket(preparePacket) .handleAndReturn( fulfillPacket -> { }, // Do nothing on fulfill this::checkForAndTriggerUnrecoverableError // check for unrecoverable error. ) // We typically expect this Prepare operation to reject, but regardless of whether the response is a fulfill or a // reject, try to read the Receiver's Connection Asset Details and return them. .map(readDetailsFromStream::apply, readDetailsFromStream::apply); } /** * Helper method to send money in a packetized operation. */ private void sendMoneyPacketized() { final AtomicBoolean timeoutReached = new AtomicBoolean(false); final ScheduledExecutorService timeoutMonitor = Executors.newSingleThreadScheduledExecutor(); boolean tryingToSendTooMuch = false; timeout.ifPresent($ -> timeoutMonitor.schedule( () -> { timeoutReached.set(true); timeoutMonitor.shutdown(); }, $.toMillis(), TimeUnit.MILLISECONDS )); while (soldierOn(timeoutReached.get(), tryingToSendTooMuch)) { // Determine the amount to send final PrepareAmounts amounts = receiverDenomination .map(receiverDenomination -> paymentTracker.getSendPacketAmounts( congestionController.getMaxAmount(), senderDenomination, receiverDenomination )) .orElseGet(() -> paymentTracker.getSendPacketAmounts( congestionController.getMaxAmount(), senderDenomination )); UnsignedLong amountToSend = amounts.getAmountToSend(); UnsignedLong receiverMinimum = amounts.getMinimumAmountToAccept(); if (amountToSend.equals(UnsignedLong.ZERO) || timeoutReached.get() || unrecoverableErrorEncountered.get()) { try { // Don't send any more, but wait a bit for outstanding requests to complete so we don't cycle needlessly in // a while loop that doesn't do anything useful. Thread.sleep(sendPacketSleepDuration.toMillis()); } catch (InterruptedException e) { throw new StreamSenderException(e.getMessage(), e); } continue; } // Load up the STREAM packet final UnsignedLong sequence; try { sequence = this.streamConnection.nextSequence(); } catch (StreamConnectionClosedException e) { // The Connection is closed, so we can't send anything more on it. logger.warn( "Unable to send more packets on a closed StreamConnection. streamConnection={} error={}", streamConnection, e ); continue; } final List frames = Lists.newArrayList( StreamMoneyFrame.builder() // This aggregator supports only a simple stream-id, which is one. .streamId(UnsignedLong.ONE) .shares(UnsignedLong.ONE) .build() ); final StreamPacket streamPacket = StreamPacket.builder() .interledgerPacketType(InterledgerPacketType.PREPARE) // If the STREAM packet is sent on an ILP Prepare, this represents the minimum the receiver should accept. .prepareAmount(receiverMinimum) .sequence(sequence) .frames(frames) .build(); // Create the ILP Prepare packet final byte[] streamPacketData = this.toEncrypted(sharedSecret, streamPacket); final InterledgerCondition executionCondition; executionCondition = generatedFulfillableFulfillment(sharedSecret, streamPacketData).getCondition(); final Supplier preparePacket = () -> InterledgerPreparePacket.builder() .destination(destinationAddress) .amount(amountToSend) .executionCondition(executionCondition) .expiresAt(DateUtils.now().plusSeconds(30L)) .data(streamPacketData) .build(); // auth // capture // rollback final PrepareAmounts prepareAmounts = PrepareAmounts.from(preparePacket.get(), streamPacket); if (!paymentTracker.auth(prepareAmounts)) { // if we can't auth, just skip this iteration of the loop until everything else completes tryingToSendTooMuch = true; continue; } try { // don't submit new tasks if the timeout was reached within this iteration of the while loop if (!timeoutReached.get()) { // call this before spinning off a task. failing to do so can create a condition in the // soliderOn check where we will incorrectly evaluate hasInFlight on the congestion // controller to not reflect what we've actually scheduled to run, resulting in the loop // breaking prematurely congestionController.prepare(amountToSend); schedule(timeoutReached, preparePacket, streamPacket, prepareAmounts); } else { logger.error("SoldierOn runLoop had more tasks to schedule but was timed-out"); } } catch (Exception e) { // Retry this amount on the next run... paymentTracker.rollback(prepareAmounts, false); logger.error("Submit failed", e); } } timeoutMonitor.shutdownNow(); } /** * Schedules a {@link Callable} with {@link this#executorService} to actually send an {@link * InterledgerPreparePacket} on an existing Stream connection. * * @param timeoutReached An {@link AtomicBoolean} that can be set to indicate whether a timeout has been * reached. * @param preparePacketSupplier A {@link Supplier} of the {@link InterledgerPreparePacket} that will be used by this * method. This is a supplier in order to facilitate late-bound construction of the * packet in order to ensure the when a packet is constructed, its expiry is * initialized very close to when the packet will actually be sent on a link. Without * this supplier, packets were getting created and then handing around in the executor. * Under extreme-load conditions, these packets would expire before ever getting sent * out over the wire. * @param streamPacket A decoded {@link StreamPacket} containing all STREAM information that is inside of * the Prepare packet passed into this method. This is provided for debugging and minor * optimization improvements inside of nested methods called by this method. * @param prepareAmounts A {@link PrepareAmounts} for augmenting the sending of the prepare packet. */ @VisibleForTesting void schedule( final AtomicBoolean timeoutReached, final Supplier preparePacketSupplier, final StreamPacket streamPacket, final PrepareAmounts prepareAmounts ) { Objects.requireNonNull(timeoutReached); Objects.requireNonNull(preparePacketSupplier); Objects.requireNonNull(streamPacket); Objects.requireNonNull(prepareAmounts); try { executorService.submit(() -> { InterledgerPreparePacket preparePacket = preparePacketSupplier.get(); if (!timeoutReached.get()) { try { link.sendPacket(preparePacket).handle( fulfillPacket -> handleFulfill(preparePacket, streamPacket, fulfillPacket, prepareAmounts), rejectPacket -> handleReject( preparePacket, streamPacket, rejectPacket, prepareAmounts, numRejectedPackets, congestionController ) ); } catch (Exception e) { logger.error("Link send failed. preparePacket={}", preparePacket, e); congestionController.reject(preparePacket.getAmount(), InterledgerRejectPacket.builder() .code(InterledgerErrorCode.F00_BAD_REQUEST) .message( String.format("Link send failed. preparePacket=%s error=%s", preparePacket, e.getMessage()) ) .build()); paymentTracker.rollback(prepareAmounts, false); } } else { logger.info("timeout reached, not sending packet"); congestionController.reject(preparePacket.getAmount(), InterledgerRejectPacket.builder() .code(InterledgerErrorCode.R00_TRANSFER_TIMED_OUT) .message(String.format("Timeout reached before packet could be sent", preparePacket)) .build()); } }); } catch (RejectedExecutionException e) { // If we get here, it means the task was unable to be scheduled, so we need to unwind the congestion // controller to prevent deadlock. congestionController.reject(preparePacketSupplier.get().getAmount(), InterledgerRejectPacket.builder() .code(InterledgerErrorCode.F00_BAD_REQUEST) .message( String .format("Unable to schedule sendMoney task. preparePacket=%s error=%s", preparePacketSupplier.get(), e.getMessage()) ) .build()); throw e; } } @VisibleForTesting boolean soldierOn(final boolean timeoutReached, final boolean tryingToSendTooMuch) { // if money in flight, always soldier on // otherwise, soldier on if // the connection is not closed // and you haven't delivered the full amount // and you haven't timed out // and you're not trying to send too much // and we haven't hit an unrecoverable error return this.congestionController.hasInFlight() || (!streamConnection.isClosed() && paymentTracker.moreToSend() && !timeoutReached && !tryingToSendTooMuch && !unrecoverableErrorEncountered.get()); } /** * Convert a {@link StreamPacket} to bytes using the CodecContext and then encrypt it using the supplied {@code * sharedSecret}. * * @param sharedSecret The shared secret known only to this client and the remote STREAM receiver, used to encrypt * and decrypt STREAM frames and packets sent and received inside of ILPv4 packets sent over the * Interledger between these two entities (i.e., sender and receiver). * @param streamPacket A {@link StreamPacket} to encode into ASN.1 OER and then encrypt into a byte array. * * @return A byte-array containing the encrypted version of an ASN.1 OER encoded {@link StreamPacket}. */ @VisibleForTesting byte[] toEncrypted(final SharedSecret sharedSecret, final StreamPacket streamPacket) { Objects.requireNonNull(sharedSecret); Objects.requireNonNull(streamPacket); try { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); streamCodecContext.write(streamPacket, baos); final byte[] streamPacketBytes = baos.toByteArray(); return streamEncryptionService.encrypt(sharedSecret, streamPacketBytes); } catch (IOException e) { throw new StreamSenderException(e.getMessage(), e); } } /** * Convert the encrypted bytes of a stream packet into a {@link StreamPacket} using the CodecContext and {@code * sharedSecret}. * * @param sharedSecret The shared secret known only to this client and the remote STREAM receiver, * used to encrypt and decrypt STREAM frames and packets sent and received inside * of ILPv4 packets sent over the Interledger between these two entities (i.e., * sender and receiver). * @param encryptedStreamPacketBytes A byte-array containing an encrypted ASN.1 OER encoded {@link StreamPacket}. * * @return The decrypted {@link StreamPacket}. */ @VisibleForTesting StreamPacket fromEncrypted(final SharedSecret sharedSecret, final byte[] encryptedStreamPacketBytes) { Objects.requireNonNull(sharedSecret); Objects.requireNonNull(encryptedStreamPacketBytes); final byte[] streamPacketBytes = this.streamEncryptionService.decrypt(sharedSecret, encryptedStreamPacketBytes); try { return streamCodecContext.read(StreamPacket.class, new ByteArrayInputStream(streamPacketBytes)); } catch (IOException e) { throw new StreamSenderException(e.getMessage(), e); } } @VisibleForTesting void handleFulfill( final InterledgerPreparePacket originalPreparePacket, final StreamPacket originalStreamPacket, final InterledgerFulfillPacket fulfillPacket, final PrepareAmounts prepareAmounts ) { Objects.requireNonNull(originalPreparePacket); Objects.requireNonNull(originalStreamPacket); Objects.requireNonNull(fulfillPacket); Objects.requireNonNull(prepareAmounts); this.numFulfilledPackets.getAndIncrement(); this.congestionController.fulfill(originalPreparePacket.getAmount()); this.shouldSendSourceAddress.set(false); final StreamPacket streamPacket = this.fromEncrypted(sharedSecret, fulfillPacket.getData()); //if let Ok (packet) = StreamPacket::from_encrypted ( & self.shared_secret, fulfill.into_data()){ if (streamPacket.interledgerPacketType() == InterledgerPacketType.FULFILL) { UnsignedLong deliveredAmount = streamPacket.prepareAmount(); paymentTracker.commit(prepareAmounts, deliveredAmount); } else { logger.warn("Unable to parse STREAM packet from fulfill data. " + "originalPreparePacket={} originalStreamPacket={} fulfillPacket={}", originalPreparePacket, originalStreamPacket, fulfillPacket); } logger.debug("Prepare packet fulfilled ({} left to send). " + "originalPreparePacket={} originalStreamPacket={} fulfillPacket={}", paymentTracker.getOriginalAmountLeft(), originalPreparePacket, originalStreamPacket, fulfillPacket ); } /** * Handle a rejection packet. * * @param originalPreparePacket The {@link InterledgerPreparePacket} that triggered this rejection. * @param originalStreamPacket The {@link StreamPacket} that was inside of {@code originalPreparePacket}. * @param rejectPacket The {@link InterledgerRejectPacket} received from a peer directly connected via a * {@link Link}. * @param numRejectedPackets An {@link AtomicInteger} that holds the total number of packets rejected thus far on * this sendMoney operation. * @param congestionController The {@link CongestionController} used for this sendMoney operation. */ @VisibleForTesting void handleReject( final InterledgerPreparePacket originalPreparePacket, final StreamPacket originalStreamPacket, final InterledgerRejectPacket rejectPacket, final PrepareAmounts prepareAmounts, final AtomicInteger numRejectedPackets, final CongestionController congestionController ) { Objects.requireNonNull(originalPreparePacket); Objects.requireNonNull(originalStreamPacket); Objects.requireNonNull(rejectPacket); Objects.requireNonNull(numRejectedPackets); Objects.requireNonNull(congestionController); Objects.requireNonNull(prepareAmounts); final UnsignedLong amountToSend = originalPreparePacket.getAmount(); numRejectedPackets.getAndIncrement(); paymentTracker.rollback(prepareAmounts, true); congestionController.reject(amountToSend, rejectPacket); logger.debug( "Prepare with amount {} was rejected with code: {} ({} left to send). originalPreparePacket={} " + "originalStreamPacket={} rejectPacket={}", amountToSend, rejectPacket.getCode().getCode(), paymentTracker.getOriginalAmountLeft(), originalPreparePacket, originalStreamPacket, rejectPacket ); this.checkForAndTriggerUnrecoverableError(rejectPacket); //////////// // Log the rejection //////////// if (ErrorFamily.FINAL.equals(rejectPacket.getCode().getErrorFamily())) { // Most Final errors trigger immediate stoppage of send operations, except for F08 and F99. if (NON_TERMINAL_ERROR_CODES.contains(rejectPacket.getCode())) { // error was a tolerable F rejection (currently F08 or F99) logger.debug( "Encountered an expected ILPv4 FINAL error. Retrying... " + "originalPreparePacket={} originalStreamPacket={} rejectPacket={}", originalPreparePacket, originalStreamPacket, rejectPacket); } else { logger.error( "Encountered an unexpected ILPv4 FINAL error. Aborting this sendMoney. " + "originalPreparePacket={} originalStreamPacket={} rejectPacket={}", originalPreparePacket, originalStreamPacket, rejectPacket ); } } else if (ErrorFamily.RELATIVE.equals(rejectPacket.getCode().getErrorFamily())) { // All Relative errors trigger immediate stoppage of send operations. logger.warn( "Relative ILPv4 transport outage. originalPreparePacket={} originalStreamPacket={} rejectPacket={}", originalPreparePacket, originalStreamPacket, rejectPacket ); } else { // if (ErrorFamily.TEMPORARY.equals(rejectPacket.getCode().getErrorFamily())) { logger.warn( "Temporary ILPv4 transport outage. originalPreparePacket={} originalStreamPacket={} rejectPacket={}", originalPreparePacket, originalStreamPacket, rejectPacket ); } } /** *

A helper method to centralize all logic around when to trigger an "unrecoverable error" that will stop this * sender from schedule further packets.

* *

This method functions according to the following rules:

* *

Interledger Reject packets with the following attributes will immediately trigger an UNRECOVERABLE_ERROR * condition, stopping the sendMoney operation:

*
    *
  1. Any F-family rejections (except F08 and F99, which are used for congestion control and rate-probing).
  2. *
  3. Any R-family rejections.
  4. *
* *

Interledger Reject packets with the following attributes MAY trigger an UNRECOVERABLE_ERROR condition after * some threshold (this logic is not yet implemented, and may never be):

*
    *
  1. T00 Errors (generally should be accepted up to some threshold)
  2. *
  3. T01 Errors (generally should be accepted up to some threshold)
  4. *
  5. F99 Errors (generally should be accepted up to some threshold; used for FX problems where prepare amount is * less than min amount the receiver should receive).
  6. *
* *

Interledger Reject packets with the following attributes will NEVER trigger an UNRECOVERABLE_ERROR condition:

*
    *
  1. T02-T99 Errors (these may resolve with time)
  2. *
  3. F08 Errors (used for congestion control)
  4. *
* * @param rejectPacket An {@link InterledgerRejectPacket} that was received from an outgoing {@link Link} in * response to a prepare packet. */ @VisibleForTesting void checkForAndTriggerUnrecoverableError(final InterledgerRejectPacket rejectPacket) { Objects.requireNonNull(rejectPacket); if (ErrorFamily.FINAL.equals(rejectPacket.getCode().getErrorFamily())) { // Most Final errors trigger immediate stoppage of send operations, except for F08 and F99. if (NON_TERMINAL_ERROR_CODES.contains(rejectPacket.getCode())) { // error was a tolerable F rejection (currently F08 or F99) } else { unrecoverableErrorEncountered.set(true); } } else if (ErrorFamily.RELATIVE.equals(rejectPacket.getCode().getErrorFamily())) { // All Relative errors trigger immediate stoppage of send operations. unrecoverableErrorEncountered.set(true); } else { // if (ErrorFamily.TEMPORARY.equals(rejectPacket.getCode().getErrorFamily())) { // do nothing. } } @VisibleForTesting protected boolean isUnrecoverableErrorEncountered() { return this.unrecoverableErrorEncountered.get(); } @VisibleForTesting void setUnrecoverableErrorEncountered(boolean unrecoverableErrorEncountered) { this.unrecoverableErrorEncountered.set(unrecoverableErrorEncountered); } // TODO: FIXME per https://github.com/hyperledger/quilt/issues/308. Until this is completed, parallel STREAM send() // operations are not thread-safe. Consider a new instance of everything on each sendMoney? In other words, each // SendMoney requires its own connection information, // so as long as two senders don't use the same Connection details, everything should be thread-safe. ///** // * Close the current STREAM connection by sending a {@link ConnectionCloseFrame} to the receiver. // * // * @return An {@link UnsignedLong} representing the amount delivered by this individual stream. // */ // TODO: Add unit test coverage here per See https://github.com/hyperledger/quilt/issues/308 // @VisibleForTesting // void closeStream() throws StreamConnectionClosedException { // this.sendStreamFramesInZeroValuePacket(Lists.newArrayList( // StreamCloseFrame.builder() // .streamId(UnsignedLong.ONE) // .errorCode(ErrorCode.NoError) // .build() // )); // } // /** // * Close the current STREAM connection by sending a {@link ConnectionCloseFrame} to the receiver. // * // * @return An {@link UnsignedLong} representing the amount delivered by this individual stream. // */ // // TODO: Add unit test coverage here per See https://github.com/hyperledger/quilt/issues/308 // @VisibleForTesting // void closeConnection() throws StreamConnectionClosedException { // this.sendStreamFramesInZeroValuePacket(Lists.newArrayList( // ConnectionCloseFrame.builder() // .errorCode(ErrorCode.NoError) // .build() // )); // } // private void sendStreamFramesInZeroValuePacket(final Collection streamFrames) // throws StreamConnectionClosedException { // Objects.requireNonNull(streamFrames); // // if (streamFrames.size() <= 0) { // logger.warn("sendStreamFrames called with 0 frames"); // return; // } // // final StreamPacket streamPacket = StreamPacket.builder() // .interledgerPacketType(InterledgerPacketType.PREPARE) // .prepareAmount(UnsignedLong.ZERO) // .sequence(streamConnection.nextSequence()) // .addAllFrames(streamFrames) // .build(); // // // Create the ILP Prepare packet using an encrypted StreamPacket as the encryptedStreamPacket payload... // final byte[] encryptedStreamPacket = this.toEncrypted(sharedSecret, streamPacket); // final InterledgerCondition executionCondition; // executionCondition = generatedFulfillableFulfillment(sharedSecret, encryptedStreamPacket).getCondition(); // // final InterledgerPreparePacket preparePacket = InterledgerPreparePacket.builder() // .destination(destinationAddress) // .amount(UnsignedLong.ZERO) // .executionCondition(executionCondition) // .expiresAt(TimeUtils.now().plusSeconds(30L)) // .data(encryptedStreamPacket) // .build(); // // final PrepareAmounts prepareAmounts = PrepareAmounts.from(preparePacket, streamPacket); // // link.sendPacket(preparePacket).handle( // fulfillPacket -> handleFulfill(preparePacket, streamPacket, fulfillPacket, prepareAmounts), // rejectPacket -> handleReject(preparePacket, streamPacket, rejectPacket, prepareAmounts, numRejectedPackets, // congestionController) // ); // // // Mark the streamConnection object as closed if the caller supplied a ConnectionCloseFrame // streamFrames.stream() // .filter(streamFrame -> streamFrame.streamFrameType() == StreamFrameType.ConnectionClose) // .findAny() // .ifPresent($ -> { // streamConnection.closeConnection(); // logger.info("STREAM Connection closed."); // }); // // // Emit a log statement if the called supplied a StreamCloseFrame // streamFrames.stream() // .filter(streamFrame -> streamFrame.streamFrameType() == StreamFrameType.StreamClose) // .findAny() // .map($ -> (StreamCloseFrame) $) // .ifPresent($ -> { // logger.info( // "StreamId {} Closed. Delivered: {} ({} packets fulfilled, {} packets rejected)", // $.streamId(), paymentTracker.getDeliveredAmountInReceieverUnits(), this.numFulfilledPackets.get(), // this.numRejectedPackets.get() // ); // }); // } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy