Commit da3a84c2 authored by Daniel Connolly's avatar Daniel Connolly

remove payment channels, we are not certain they work correctly.

parent c3362447
/*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.jni;
import org.bitcoinj.protocols.channels.PaymentChannelServerListener;
import org.bitcoinj.protocols.channels.ServerConnectionEventHandler;
import javax.annotation.Nullable;
import java.net.SocketAddress;
/**
* An event listener that relays events to a native C++ object. A pointer to that object is stored in
* this class using JNI on the native side, thus several instances of this can point to different actual
* native implementations.
*/
public class NativePaymentChannelHandlerFactory implements PaymentChannelServerListener.HandlerFactory {
public long ptr;
@Nullable
@Override
public native ServerConnectionEventHandler onNewConnection(SocketAddress clientAddress);
}
/*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.jni;
import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException;
import org.bitcoinj.protocols.channels.ServerConnectionEventHandler;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
/**
* An event listener that relays events to a native C++ object. A pointer to that object is stored in
* this class using JNI on the native side, thus several instances of this can point to different actual
* native implementations.
*/
public class NativePaymentChannelServerConnectionEventHandler extends ServerConnectionEventHandler {
public long ptr;
@Override
public native void channelOpen(Sha256Hash channelId);
@Override
public native ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, ByteString info);
@Override
public native void channelClosed(PaymentChannelCloseException.CloseReason reason);
}
......@@ -23,7 +23,6 @@ import com.subgraph.orchid.*;
import org.bitcoinj.core.listeners.*;
import org.bitcoinj.core.*;
import org.bitcoinj.net.discovery.*;
import org.bitcoinj.protocols.channels.*;
import org.bitcoinj.store.*;
import org.bitcoinj.wallet.*;
import org.slf4j.*;
......@@ -328,7 +327,6 @@ public class WalletAppKit extends AbstractIdleService {
vPeerGroup.start();
// Make sure we shut down cleanly.
installShutdownHook();
completeExtensionInitiations(vPeerGroup);
// TODO: Be able to use the provided download listener when doing a blocking startup.
final DownloadProgressTracker listener = new DownloadProgressTracker();
......@@ -338,7 +336,6 @@ public class WalletAppKit extends AbstractIdleService {
Futures.addCallback(vPeerGroup.startAsync(), new FutureCallback() {
@Override
public void onSuccess(@Nullable Object result) {
completeExtensionInitiations(vPeerGroup);
final DownloadProgressTracker l = downloadListener == null ? new DownloadProgressTracker() : downloadListener;
vPeerGroup.startBlockChainDownload(l);
}
......@@ -437,24 +434,6 @@ public class WalletAppKit extends AbstractIdleService {
}
}
/*
* As soon as the transaction broadcaster han been created we will pass it to the
* payment channel extensions
*/
private void completeExtensionInitiations(TransactionBroadcaster transactionBroadcaster) {
StoredPaymentChannelClientStates clientStoredChannels = (StoredPaymentChannelClientStates)
vWallet.getExtensions().get(StoredPaymentChannelClientStates.class.getName());
if(clientStoredChannels != null) {
clientStoredChannels.setTransactionBroadcaster(transactionBroadcaster);
}
StoredPaymentChannelServerStates serverStoredChannels = (StoredPaymentChannelServerStates)
vWallet.getExtensions().get(StoredPaymentChannelServerStates.class.getName());
if(serverStoredChannels != null) {
serverStoredChannels.setTransactionBroadcaster(transactionBroadcaster);
}
}
protected PeerGroup createPeerGroup() throws TimeoutException {
if (useTor) {
TorClient torClient = new TorClient();
......
/*
* Copyright 2013 Google Inc.
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
/**
* A class implementing this interface supports the basic operations of a payment channel. An implementation is provided
* in {@link PaymentChannelClient}, but alternative implementations are possible. For example, an implementor might
* send RPCs to a separate (locally installed or even remote) wallet app rather than implementing the algorithm locally.
*/
public interface IPaymentChannelClient {
/**
* Called when a message is received from the server. Processes the given message and generates events based on its
* content.
*/
void receiveMessage(Protos.TwoWayChannelMessage msg) throws InsufficientMoneyException;
/**
* <p>Called when the connection to the server terminates.</p>
*
* <p>For stateless protocols, this translates to a client not using the channel for the immediate future, but
* intending to reopen the channel later. There is likely little reason to use this in a stateless protocol.</p>
*
* <p>Note that this <b>MUST</b> still be called even after either
* {@link ClientConnection#destroyConnection(org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason)} or
* {@link IPaymentChannelClient#settle()} is called, to actually handle the connection close logic.</p>
*/
void connectionClosed();
/**
* <p>Settles the channel, notifying the server it can broadcast the most recent payment transaction.</p>
*
* <p>Note that this only generates a CLOSE message for the server and calls
* {@link ClientConnection#destroyConnection(org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason)}
* to settle the connection, it does not actually handle connection close logic, and
* {@link PaymentChannelClient#connectionClosed()} must still be called after the connection fully settles.</p>
*
* @throws IllegalStateException If the connection is not currently open (ie the CLOSE message cannot be sent)
*/
void settle() throws IllegalStateException;
/**
* <p>Called to indicate the connection has been opened and messages can now be generated for the server.</p>
*
* <p>Attempts to find a channel to resume and generates a CLIENT_VERSION message for the server based on the
* result.</p>
*/
void connectionOpen();
/**
* Increments the total value which we pay the server. Note that the amount of money sent may not be the same as the
* amount of money actually requested. It can be larger if the amount left over in the channel would be too small to
* be accepted by the Bitcoin network. ValueOutOfRangeException will be thrown, however, if there's not enough money
* left in the channel to make the payment at all. Only one payment can be in-flight at once. You have to ensure
* you wait for the previous increase payment future to complete before incrementing the payment again.
*
* @param size How many satoshis to increment the payment by (note: not the new total).
* @param info Information about this update, used to extend this protocol.
* @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value
* ({@link PaymentChannelClientConnection#state()}.getTotalValue())
* @throws IllegalStateException If the channel has been closed or is not yet open
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
* @throws ECKey.KeyIsEncryptedException If the keys are encrypted and no AES key has been provided,
* @return a future that completes when the server acknowledges receipt and acceptance of the payment.
*/
ListenableFuture<PaymentIncrementAck> incrementPayment(Coin size, @Nullable ByteString info,
@Nullable KeyParameter userKey)
throws ValueOutOfRangeException, IllegalStateException, ECKey.KeyIsEncryptedException;
/**
* Implements the connection between this client and the server, providing an interface which allows messages to be
* sent to the server, requests for the connection to the server to be closed, and a callback which occurs when the
* channel is fully open.
*/
interface ClientConnection {
/**
* <p>Requests that the given message be sent to the server. There are no blocking requirements for this method,
* however the order of messages must be preserved.</p>
*
* <p>If the send fails, no exception should be thrown, however
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#connectionClosed()} should be called immediately. In the case of messages which
* are a part of initialization, initialization will simply fail and the refund transaction will be broadcasted
* when it unlocks (if necessary). In the case of a payment message, the payment will be lost however if the
* channel is resumed it will begin again from the channel value <i>after</i> the failed payment.</p>
*
* <p>Called while holding a lock on the {@link org.bitcoinj.protocols.channels.PaymentChannelClient} object - be careful about reentrancy</p>
*/
void sendToServer(Protos.TwoWayChannelMessage msg);
/**
* <p>Requests that the connection to the server be closed. For stateless protocols, note that after this call,
* no more messages should be received from the server and this object is no longer usable. A
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#connectionClosed()} event should be generated immediately after this call.</p>
*
* <p>Called while holding a lock on the {@link org.bitcoinj.protocols.channels.PaymentChannelClient} object - be careful about reentrancy</p>
*
* @param reason The reason for the closure, see the individual values for more details.
* It is usually safe to ignore this and treat any value below
* {@link org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason#CLIENT_REQUESTED_CLOSE} as "unrecoverable error" and all others as
* "try again once and see if it works then"
*/
void destroyConnection(PaymentChannelCloseException.CloseReason reason);
/**
* <p>Queries if the expire time proposed by server is acceptable. If <code>false</code> is return the channel
* will be closed with a {@link org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason#TIME_WINDOW_UNACCEPTABLE}.</p>
* @param expireTime The time, in seconds, when this channel will be closed by the server. Note this is in absolute time, i.e. seconds since 1970-01-01T00:00:00.
* @return <code>true</code> if the proposed time is acceptable <code>false</code> otherwise.
*/
boolean acceptExpireTime(long expireTime);
/**
* <p>Indicates the channel has been successfully opened and
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#incrementPayment(Coin)}
* may be called at will.</p>
*
* <p>Called while holding a lock on the {@link org.bitcoinj.protocols.channels.PaymentChannelClient}
* object - be careful about reentrancy</p>
*
* @param wasInitiated If true, the channel is newly opened. If false, it was resumed.
*/
void channelOpen(boolean wasInitiated);
}
/**
* An implementor of this interface creates payment channel clients that "talk back" with the given connection.
* The client might be a PaymentChannelClient, or an RPC interface, or something else entirely.
*/
interface Factory {
IPaymentChannelClient create(String serverPaymentIdentity, ClientConnection connection);
}
}
/*
* Copyright 2013 Google Inc.
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
/**
* Used to indicate that a channel was closed before it was expected to be closed.
* This could mean the connection timed out, the other send sent an error or a CLOSE message, etc
*/
public class PaymentChannelCloseException extends Exception {
public enum CloseReason {
/** We could not find a version which was mutually acceptable with the client/server */
NO_ACCEPTABLE_VERSION,
/** Generated by the client when time window the suggested by the server is unacceptable */
TIME_WINDOW_UNACCEPTABLE,
/** Generated by the client when the server requested we lock up an unacceptably high value */
SERVER_REQUESTED_TOO_MUCH_VALUE,
/** Generated by the server when the client has used up all the value in the channel. */
CHANNEL_EXHAUSTED,
// Values after here indicate its probably possible to try reopening channel again
/**
* <p>The {@link org.bitcoinj.protocols.channels.PaymentChannelClient#settle()} method was called or the
* client sent a CLOSE message.</p>
* <p>As long as the server received the CLOSE message, this means that the channel is settling and the payment
* transaction (if any) will be broadcast. If the client attempts to open a new connection, a new channel will
* have to be opened.</p>
*/
CLIENT_REQUESTED_CLOSE,
/**
* <p>The {@link org.bitcoinj.protocols.channels.PaymentChannelServer#close()} method was called or server
* sent a CLOSE message.</p>
*
* <p>This may occur if the server opts to close the connection for some reason, or automatically if the channel
* times out (called by {@link StoredPaymentChannelServerStates}).</p>
*
* <p>For a client, this usually indicates that we should try again if we need to continue paying (either
* opening a new channel or continuing with the same one depending on the server's preference)</p>
*/
SERVER_REQUESTED_CLOSE,
/** Remote side sent an ERROR message */
REMOTE_SENT_ERROR,
/** Remote side sent a message we did not understand */
REMOTE_SENT_INVALID_MESSAGE,
/** The connection was closed without an ERROR/CLOSE message */
CONNECTION_CLOSED,
/** The server failed processing an UpdatePayment message */
UPDATE_PAYMENT_FAILED,
}
private final CloseReason error;
public PaymentChannelCloseException(String message, CloseReason error) {
super(message);
this.error = error;
}
public CloseReason getCloseReason() {
return error;
}
@Override
public String toString() {
return "PaymentChannelCloseException for reason " + getCloseReason();
}
}
/*
* Copyright 2013 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.TransactionBroadcaster;
import org.bitcoinj.net.NioServer;
import org.bitcoinj.net.ProtobufConnection;
import org.bitcoinj.net.StreamConnectionFactory;
import org.bitcoinj.wallet.Wallet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Implements a listening TCP server that can accept connections from payment channel clients, and invokes the provided
* event listeners when new channels are opened or payments arrive. This is the highest level class in the payment
* channels API. Internally, sends protobuf messages to/from a newly created {@link PaymentChannelServer}.
*/
public class PaymentChannelServerListener {
// The wallet and peergroup which are used to complete/broadcast transactions
private final Wallet wallet;
private final TransactionBroadcaster broadcaster;
// The event handler factory which creates new ServerConnectionEventHandler per connection
private final HandlerFactory eventHandlerFactory;
private final Coin minAcceptedChannelSize;
private NioServer server;
private final int timeoutSeconds;
/**
* A factory which generates connection-specific event handlers.
*/
public interface HandlerFactory {
/**
* Called when a new connection completes version handshake to get a new connection-specific listener.
* If null is returned, the connection is immediately closed.
*/
@Nullable
ServerConnectionEventHandler onNewConnection(SocketAddress clientAddress);
}
private class ServerHandler {
public ServerHandler(final SocketAddress address, final int timeoutSeconds) {
paymentChannelManager = new PaymentChannelServer(broadcaster, wallet, minAcceptedChannelSize, new PaymentChannelServer.ServerConnection() {
@Override public void sendToClient(Protos.TwoWayChannelMessage msg) {
socketProtobufHandler.write(msg);
}
@Override public void destroyConnection(PaymentChannelCloseException.CloseReason reason) {
if (closeReason != null)
closeReason = reason;
socketProtobufHandler.closeConnection();
}
@Override public void channelOpen(Sha256Hash contractHash) {
socketProtobufHandler.setSocketTimeout(0);
eventHandler.channelOpen(contractHash);
}
@Override public ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, @Nullable ByteString info) {
return eventHandler.paymentIncrease(by, to, info);
}
});
protobufHandlerListener = new ProtobufConnection.Listener<Protos.TwoWayChannelMessage>() {
@Override
public synchronized void messageReceived(ProtobufConnection<Protos.TwoWayChannelMessage> handler, Protos.TwoWayChannelMessage msg) {
paymentChannelManager.receiveMessage(msg);
}
@Override
public synchronized void connectionClosed(ProtobufConnection<Protos.TwoWayChannelMessage> handler) {
paymentChannelManager.connectionClosed();
if (closeReason != null)
eventHandler.channelClosed(closeReason);
else
eventHandler.channelClosed(PaymentChannelCloseException.CloseReason.CONNECTION_CLOSED);
eventHandler.setConnectionChannel(null);
}
@Override
public synchronized void connectionOpen(ProtobufConnection<Protos.TwoWayChannelMessage> handler) {
ServerConnectionEventHandler eventHandler = eventHandlerFactory.onNewConnection(address);
if (eventHandler == null)
handler.closeConnection();
else {
ServerHandler.this.eventHandler = eventHandler;
ServerHandler.this.eventHandler.setConnectionChannel(socketProtobufHandler);
paymentChannelManager.connectionOpen();
}
}
};
socketProtobufHandler = new ProtobufConnection<Protos.TwoWayChannelMessage>
(protobufHandlerListener, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, timeoutSeconds*1000);
}
private PaymentChannelCloseException.CloseReason closeReason;
// The user-provided event handler
private ServerConnectionEventHandler eventHandler;
// The payment channel server which does the actual payment channel handling
private final PaymentChannelServer paymentChannelManager;
// The connection handler which puts/gets protobufs from the TCP socket
private final ProtobufConnection<Protos.TwoWayChannelMessage> socketProtobufHandler;
// The listener which connects to socketProtobufHandler
private final ProtobufConnection.Listener<Protos.TwoWayChannelMessage> protobufHandlerListener;
}
/**
* Binds to the given port and starts accepting new client connections.
* @throws Exception If binding to the given port fails (eg SocketException: Permission denied for privileged ports)
*/
public void bindAndStart(int port) throws Exception {
server = new NioServer(new StreamConnectionFactory() {
@Override
public ProtobufConnection<Protos.TwoWayChannelMessage> getNewConnection(InetAddress inetAddress, int port) {
return new ServerHandler(new InetSocketAddress(inetAddress, port), timeoutSeconds).socketProtobufHandler;
}
}, new InetSocketAddress(port));
server.startAsync();
server.awaitRunning();
}
/**
* Sets up a new payment channel server which listens on the given port.
*
* @param broadcaster The PeerGroup on which transactions will be broadcast - should have multiple connections.
* @param wallet The wallet which will be used to complete transactions
* @param timeoutSeconds The read timeout between messages. This should accommodate latency and client ECDSA
* signature operations.
* @param minAcceptedChannelSize The minimum amount of coins clients must lock in to create a channel. Clients which
* are unwilling or unable to lock in at least this value will immediately disconnect.
* For this reason, a fairly conservative value (in terms of average value spent on a
* channel) should generally be chosen.
* @param eventHandlerFactory A factory which generates event handlers which are created for each new connection
*/
public PaymentChannelServerListener(TransactionBroadcaster broadcaster, Wallet wallet,
final int timeoutSeconds, Coin minAcceptedChannelSize,
HandlerFactory eventHandlerFactory) throws IOException {
this.wallet = checkNotNull(wallet);
this.broadcaster = checkNotNull(broadcaster);
this.eventHandlerFactory = checkNotNull(eventHandlerFactory);
this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize);
this.timeoutSeconds = timeoutSeconds;
}
/**
* <p>Closes all client connections currently connected gracefully.</p>
*
* <p>Note that this does <i>not</i> settle the actual payment channels (and broadcast payment transactions), which
* must be done using the {@link StoredPaymentChannelServerStates} which manages the states for the associated
* wallet.</p>
*/
public void close() {
server.stopAsync();
server.awaitTerminated();
}
}
/*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import org.bitcoinj.core.Coin;
import com.google.protobuf.ByteString;
import javax.annotation.Nullable;
/**
* An acknowledgement of a payment increase
*/
public class PaymentIncrementAck {
private final Coin value;
@Nullable private final ByteString info;
public PaymentIncrementAck(Coin value, @Nullable ByteString info) {
this.value = value;
this.info = info;
}
public Coin getValue() {
return value;
}
@Nullable
public ByteString getInfo() {
return info;
}
}
Using the protocol suggestion by Jeremy Spillman
1) Client connects to server and asks for a public key.
2) Server provides a fresh key. Client creates TX1 which pays to a 2-of-2 multisig output. It creates an invalid
TX2 which spends TX1 and pays all money back to itself. The refund TX is time locked.
3) Client sends TX2 to server which verifies that it's valid and not connected to any transaction in its wallet.
Server signs TX2 and sends back the signature.
4) Client verifies that the server signed TX2 correctly and then sends TX1 to the server, which verifies that it
was the tx connected to the thing it just signed, and then broadcasts it thus locking in the money.
5) Each time the channel is adjusted, the client sends a new signed TX2 to the server which keeps it (does not need
to sign itself).
If the client or server wants to close the channel, the last TX2 is broadcast. It's a normal, final transaction so
it ends the negotiation at that point.
If the server goes away and does not finalize the channel properly, the refund TX can be broadcast once the time lock
expires. Note that you cannot broadcast the refund tx before the time lock expires (thus filling the mempool) due to
the recent change to change non-final transactions non-standard. Thus TX replacement is not needed in this particular
configuration.
When TX replacement is re-activated, this configuration would become vulnerable to having the refund TX be broadcast
by the client. We can require the refund TX to have an input sequence number of zero. The adjustment transactions have
a sequence number of UINT_MAX as before, this means they would replace the refund tx if it were to be broadcast.
This configuration is less general than a full payment channel with tx replacement activated, but for our purposes
it does the trick.
/*
* Copyright 2013 Google Inc.
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/