3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 14:54:15 +00:00

Adding support for processing PaymentRequests.

This commit is contained in:
Kevin Greene 2013-10-06 13:39:42 -07:00 committed by Mike Hearn
parent 4ca476ff35
commit 3966875e8e
16 changed files with 7186 additions and 14 deletions

View File

@ -57,6 +57,11 @@ public abstract class NetworkParameters implements Serializable {
/** Unit test network. */
public static final String ID_UNITTESTNET = "com.google.bitcoin.unittest";
/** The string used by the payment protocol to represent the main net. */
public static final String PAYMENT_PROTOCOL_ID_MAINNET = "main";
/** The string used by the payment protocol to represent the test net. */
public static final String PAYMENT_PROTOCOL_ID_TESTNET = "test";
// TODO: Seed nodes should be here as well.
protected Block genesisBlock;
@ -173,6 +178,8 @@ public abstract class NetworkParameters implements Serializable {
return id;
}
public abstract String getPaymentProtocolId();
@Override
public boolean equals(Object other) {
if (!(other instanceof NetworkParameters)) return false;
@ -199,6 +206,18 @@ public abstract class NetworkParameters implements Serializable {
}
}
/** Returns the network parameters for the given string paymentProtocolID or NULL if not recognized. */
@Nullable
public static NetworkParameters fromPmtProtocolID(String pmtProtocolId) {
if (pmtProtocolId.equals(PAYMENT_PROTOCOL_ID_MAINNET)) {
return MainNetParams.get();
} else if (pmtProtocolId.equals(PAYMENT_PROTOCOL_ID_TESTNET)) {
return TestNet3Params.get();
} else {
return null;
}
}
public int getSpendableCoinbaseDepth() {
return spendableCoinbaseDepth;
}

View File

@ -72,4 +72,8 @@ public class MainNetParams extends NetworkParameters {
}
return instance;
}
public String getPaymentProtocolId() {
return PAYMENT_PROTOCOL_ID_MAINNET;
}
}

View File

@ -64,4 +64,8 @@ public class RegTestParams extends TestNet2Params {
}
return instance;
}
public String getPaymentProtocolId() {
return null;
}
}

View File

@ -55,4 +55,8 @@ public class TestNet2Params extends NetworkParameters {
}
return instance;
}
public String getPaymentProtocolId() {
return null;
}
}

View File

@ -62,4 +62,8 @@ public class TestNet3Params extends NetworkParameters {
}
return instance;
}
public String getPaymentProtocolId() {
return PAYMENT_PROTOCOL_ID_TESTNET;
}
}

View File

@ -53,4 +53,8 @@ public class UnitTestParams extends NetworkParameters {
}
return instance;
}
public String getPaymentProtocolId() {
return null;
}
}

View File

@ -0,0 +1,93 @@
/**
* 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 com.google.bitcoin.protocols.payments;
public class PaymentRequestException extends Exception {
public PaymentRequestException(String msg) {
super(msg);
}
public PaymentRequestException(Exception e) {
super(e);
}
public static class Expired extends PaymentRequestException {
public Expired(String msg) {
super(msg);
}
}
public static class InvalidPaymentRequestURL extends PaymentRequestException {
public InvalidPaymentRequestURL(String msg) {
super(msg);
}
public InvalidPaymentRequestURL(Exception e) {
super(e);
}
}
public static class InvalidPaymentURL extends PaymentRequestException {
public InvalidPaymentURL(Exception e) {
super(e);
}
}
public static class InvalidOutputs extends PaymentRequestException {
public InvalidOutputs(String msg) {
super(msg);
}
}
public static class InvalidVersion extends PaymentRequestException {
public InvalidVersion(String msg) {
super(msg);
}
}
public static class InvalidNetwork extends PaymentRequestException {
public InvalidNetwork(String msg) {
super(msg);
}
}
public static class InvalidPkiType extends PaymentRequestException {
public InvalidPkiType(String msg) {
super(msg);
}
}
public static class InvalidPkiData extends PaymentRequestException {
public InvalidPkiData(String msg) {
super(msg);
}
public InvalidPkiData(Exception e) {
super(e);
}
}
public static class PkiVerificationException extends PaymentRequestException {
public PkiVerificationException(String msg) {
super(msg);
}
public PkiVerificationException(Exception e) {
super(e);
}
}
}

View File

@ -0,0 +1,597 @@
/**
* 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 com.google.bitcoin.protocols.payments;
import com.google.bitcoin.core.*;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.TestNet3Params;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.bitcoin.protocols.payments.Protos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.asn1.ASN1String;
import org.spongycastle.asn1.x500.AttributeTypeAndValue;
import org.spongycastle.asn1.x500.RDN;
import org.spongycastle.asn1.x500.style.RFC4519Style;
import org.spongycastle.asn1.x500.X500Name;
import javax.annotation.Nullable;
import javax.security.auth.x500.X500Principal;
import java.io.*;
import java.math.BigInteger;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
/**
* <p>Provides a standard implementation of the Payment Protocol (BIP 0070)</p>
*
* <p>A PaymentSession can be initialized from one of the following:</p>
*
* <ul>
* <li>A {@link BitcoinURI} object that conforms to BIP 0072</li>
* <li>A url where the {@link Protos.PaymentRequest} can be fetched</li>
* <li>Directly with a {@link Protos.PaymentRequest} object</li>
* </ul>
*
* If initialized with a BitcoinURI or a url, a network request is made for the payment request object and a
* ListenableFuture is returned that will be notified with the PaymentSession object after it is downloaded.
*
* Once the PaymentSession is initialized, typically a wallet application will prompt the user to confirm that the
* amount and recipient are correct, perform any additional steps, and then construct a list of transactions to pass to
* the sendPayment method.
*
* Call sendPayment with a list of transactions that will be broadcast. A {@link Protos.Payment} message will be sent to
* the merchant if a payment url is provided in the PaymentRequest.
* NOTE: sendPayment does NOT broadcast the transactions to the bitcoin network.
*
* sendPayment returns a ListenableFuture that will be notified when a {@link Protos.PaymentACK} is received from the
* merchant. Typically a wallet will show the message to the user as a confirmation message that the payment is now
* "processing" or that an error occurred.
*
* @author Kevin Greene
* @see <a href="https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki">BIP 0070</a>
*/
public class PaymentSession {
private static final Logger log = LoggerFactory.getLogger(PaymentSession.class);
private ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
private NetworkParameters params;
private String trustStorePath;
private Protos.PaymentRequest paymentRequest;
private Protos.PaymentDetails paymentDetails;
private BigInteger totalValue = BigInteger.ZERO;
/**
* Stores the calculated PKI verification data, or null if none is available.
* Only valid after the session is created with verifyPki set to true, or verifyPki() is manually called.
*/
public PkiVerificationData pkiVerificationData;
/**
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri.
* uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may
* be fetched in the r= parameter.
* If the payment request object specifies a PKI method, then the system trust store will
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
* signature cannot be verified.
*/
public static ListenableFuture<PaymentSession> createFromBitcoinUri(final BitcoinURI uri) throws PaymentRequestException {
return createFromBitcoinUri(uri, true, null);
}
/**
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri.
* uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may
* be fetched in the r= parameter.
* If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
* signature cannot be verified.
*/
public static ListenableFuture<PaymentSession> createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki)
throws PaymentRequestException {
return createFromBitcoinUri(uri, verifyPki, null);
}
/**
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri.
* uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may
* be fetched in the r= parameter.
* If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
* signature cannot be verified.
* If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location
* instead of using the system default trust store location.
*/
public static ListenableFuture<PaymentSession> createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki, @Nullable final String trustStorePath)
throws PaymentRequestException {
String url = uri.getPaymentRequestUrl();
if (url == null)
throw new PaymentRequestException.InvalidPaymentRequestURL("No payment request URL (r= parameter) in BitcoinURI " + uri);
try {
return fetchPaymentRequest(new URI(url), verifyPki, trustStorePath);
} catch (URISyntaxException e) {
throw new PaymentRequestException.InvalidPaymentRequestURL(e);
}
}
/**
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url.
* url is an address where the {@link Protos.PaymentRequest} object may be fetched.
* If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
* signature cannot be verified.
*/
public static ListenableFuture<PaymentSession> createFromUrl(final String url) throws PaymentRequestException {
return createFromUrl(url, true, null);
}
/**
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url.
* url is an address where the {@link Protos.PaymentRequest} object may be fetched.
* If the payment request object specifies a PKI method, then the system trust store will
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
* signature cannot be verified.
*/
public static ListenableFuture<PaymentSession> createFromUrl(final String url, final boolean verifyPki)
throws PaymentRequestException {
return createFromUrl(url, verifyPki, null);
}
/**
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url.
* url is an address where the {@link Protos.PaymentRequest} object may be fetched.
* If the payment request object specifies a PKI method, then the system trust store will
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
* signature cannot be verified.
* If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location
* instead of using the system default trust store location.
*/
public static ListenableFuture<PaymentSession> createFromUrl(final String url, final boolean verifyPki, @Nullable final String trustStorePath)
throws PaymentRequestException {
if (url == null)
throw new PaymentRequestException.InvalidPaymentRequestURL("null paymentRequestUrl");
try {
return fetchPaymentRequest(new URI(url), true, trustStorePath);
} catch(URISyntaxException e) {
throw new PaymentRequestException.InvalidPaymentRequestURL(e);
}
}
private static ListenableFuture<PaymentSession> fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final String trustStorePath) {
ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
return executor.submit(new Callable<PaymentSession>() {
@Override
public PaymentSession call() throws Exception {
HttpURLConnection connection = (HttpURLConnection)uri.toURL().openConnection();
connection.setRequestProperty("Accept", "application/bitcoin-paymentrequest");
connection.setUseCaches(false);
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.parseFrom(connection.getInputStream());
PaymentSession paymentSession = new PaymentSession(paymentRequest, verifyPki, trustStorePath);
return paymentSession;
}
});
}
/**
* Creates a PaymentSession from the provided {@link Protos.PaymentRequest}.
* Verifies PKI by default.
*/
public PaymentSession(Protos.PaymentRequest request) throws PaymentRequestException {
parsePaymentRequest(request);
verifyPki();
}
/**
* Creates a PaymentSession from the provided {@link Protos.PaymentRequest}.
* If verifyPki is true, also validates the signature and throws an exception if it fails.
*/
public PaymentSession(Protos.PaymentRequest request, boolean verifyPki) throws PaymentRequestException {
parsePaymentRequest(request);
if (verifyPki)
verifyPki();
}
/**
* Creates a PaymentSession from the provided {@link Protos.PaymentRequest}.
* If verifyPki is true, also validates the signature and throws an exception if it fails.
* If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location
* instead of using the system default trust store location.
*/
public PaymentSession(Protos.PaymentRequest request, boolean verifyPki, @Nullable final String trustStorePath) throws PaymentRequestException {
this.trustStorePath = trustStorePath;
parsePaymentRequest(request);
if (verifyPki)
verifyPki();
}
/**
* Message returned by the merchant in response to a Payment message.
*/
public class Ack {
private String memo = "";
Ack(String memo) {
this.memo = memo;
}
/**
* Returns the memo included by the merchant in the payment ack. This message is typically displayed to the user
* as a notification (e.g. "Your payment was received and is being processed").
*/
public String getMemo() {
return memo;
}
}
/**
* Returns the memo included by the merchant in the payment request.
*/
public String getMemo() {
return paymentDetails.getMemo();
}
/**
* Returns the total amount of bitcoins requested.
*/
public BigInteger getValue() {
return totalValue;
}
/**
* Returns the date that the payment request was generated.
*/
public Date getDate() {
return new Date(paymentDetails.getTime() * 1000);
}
/**
* This should always be called before attempting to call sendPayment.
*/
public boolean isExpired() {
return paymentDetails.hasExpires() && System.currentTimeMillis() / 1000L > paymentDetails.getExpires();
}
/**
* Returns the payment url where the Payment message should be sent.
* Returns null if no payment url was provided in the PaymentRequest.
*/
public @Nullable String getPaymentUrl() {
if (paymentDetails.hasPaymentUrl())
return paymentDetails.getPaymentUrl();
return null;
}
/**
* Returns a {@link Wallet.SendRequest} suitable for broadcasting to the network.
*/
public Wallet.SendRequest getSendRequest() {
Transaction tx = new Transaction(params);
for (Protos.Output output : paymentDetails.getOutputsList())
tx.addOutput(new TransactionOutput(params, tx, BigInteger.valueOf(output.getAmount()), output.getScript().toByteArray()));
return Wallet.SendRequest.forTx(tx);
}
/**
* Generates a Payment message and sends the payment to the merchant who sent the PaymentRequest.
* Provide transactions built by the wallet.
* NOTE: This does not broadcast the transactions to the bitcoin network, it merely sends a Payment message to the
* merchant confirming the payment.
* Returns an object wrapping PaymentACK once received.
* If the PaymentRequest did not specify a payment_url, returns null and does nothing.
* @param txns list of transactions to be included with the Payment message.
* @param refundAddr will be used by the merchant to send money back if there was a problem.
* @param memo is a message to include in the payment message sent to the merchant.
*/
public @Nullable ListenableFuture<Ack> sendPayment(List<Transaction> txns, @Nullable Address refundAddr, @Nullable String memo)
throws PaymentRequestException, VerificationException, IOException {
Protos.Payment payment = getPayment(txns, refundAddr, memo);
if (payment == null)
return null;
if (isExpired())
throw new PaymentRequestException.Expired("PaymentRequest is expired");
URL url;
try {
url = new URL(paymentDetails.getPaymentUrl());
} catch (MalformedURLException e) {
throw new PaymentRequestException.InvalidPaymentURL(e);
}
return sendPayment(url, payment);
}
/**
* Generates a Payment message based on the information in the PaymentRequest.
* Provide transactions built by the wallet.
* If the PaymentRequest did not specify a payment_url, returns null.
* @param txns list of transactions to be included with the Payment message.
* @param refundAddr will be used by the merchant to send money back if there was a problem.
* @param memo is a message to include in the payment message sent to the merchant.
*/
public @Nullable Protos.Payment getPayment(List<Transaction> txns, @Nullable Address refundAddr, @Nullable String memo)
throws IOException {
if (!paymentDetails.hasPaymentUrl())
return null;
Protos.Payment.Builder payment = Protos.Payment.newBuilder();
if (paymentDetails.hasMerchantData())
payment.setMerchantData(paymentDetails.getMerchantData());
if (refundAddr != null) {
Protos.Output.Builder refundOutput = Protos.Output.newBuilder();
refundOutput.setAmount(totalValue.longValue());
refundOutput.setScript(ByteString.copyFrom(ScriptBuilder.createOutputScript(refundAddr).getProgram()));
payment.addRefundTo(refundOutput);
}
if (memo != null) {
payment.setMemo(memo);
}
for (Transaction txn : txns) {
txn.verify();
ByteArrayOutputStream o = new ByteArrayOutputStream();
txn.bitcoinSerialize(o);
payment.addTransactions(ByteString.copyFrom(o.toByteArray()));
}
return payment.build();
}
@VisibleForTesting
protected ListenableFuture<Ack> sendPayment(final URL url, final Protos.Payment payment) {
return executor.submit(new Callable<Ack>() {
@Override
public Ack call() throws Exception {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/bitcoin-payment");
connection.setRequestProperty("Accept", "application/bitcoin-paymentack");
connection.setRequestProperty("Content-Length", Integer.toString(payment.getSerializedSize()));
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
// Send request.
DataOutputStream outStream = new DataOutputStream(connection.getOutputStream());
payment.writeTo(outStream);
outStream.flush();
outStream.close();
// Get response.
InputStream inStream = connection.getInputStream();
Protos.PaymentACK.Builder paymentAckBuilder = Protos.PaymentACK.newBuilder().mergeFrom(inStream);
Protos.PaymentACK paymentAck = paymentAckBuilder.build();
String memo = "";
if (paymentAck.hasMemo())
memo = paymentAck.getMemo();
return new Ack(memo);
}
});
}
/**
* Information about the X509 signature's issuer and subject.
*/
public static class PkiVerificationData {
public String name;
public PublicKey merchantSigningKey;
public TrustAnchor rootAuthority;
public String orgName;
}
/**
* Uses the provided PKI method to find the corresponding public key and verify the provided signature.
* Returns null if no PKI method was specified in the {@link Protos.PaymentRequest}.
*/
public @Nullable PkiVerificationData verifyPki() throws PaymentRequestException {
try {
if (pkiVerificationData != null)
return pkiVerificationData;
if (paymentRequest.getPkiType().equals("none"))
// Nothing to verify. Everything is fine. Move along.
return null;
String algorithm;
if (paymentRequest.getPkiType().equals("x509+sha256"))
algorithm = "SHA256withRSA";
else if (paymentRequest.getPkiType().equals("x509+sha1"))
algorithm = "SHA1withRSA";
else
throw new PaymentRequestException.InvalidPkiType("Unsupported PKI type: " + paymentRequest.getPkiType());
Protos.X509Certificates protoCerts = Protos.X509Certificates.parseFrom(paymentRequest.getPkiData());
if (protoCerts.getCertificateCount() == 0)
throw new PaymentRequestException.InvalidPkiData("No certificates provided in message: server config error");
// Parse the certs and turn into a certificate chain object. Cert factories can parse both DER and base64.
// The ordering of certificates is defined by the payment protocol spec to be the same as what the Java
// crypto API requires - convenient!
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
List<X509Certificate> certs = Lists.newArrayList();
for (ByteString bytes : protoCerts.getCertificateList())
certs.add((X509Certificate) certificateFactory.generateCertificate(bytes.newInput()));
CertPath path = certificateFactory.generateCertPath(certs);
// Retrieves the most-trusted CAs from keystore.
PKIXParameters params = new PKIXParameters(createKeyStore(trustStorePath));
// Revocation not supported in the current version.
params.setRevocationEnabled(false);
// Now verify the certificate chain is correct and trusted. This let's us get an identity linked pubkey.
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(path, params);
PublicKey publicKey = result.getPublicKey();
// OK, we got an identity, now check it was used to sign this message.
Signature signature = Signature.getInstance(algorithm);
// Note that we don't use signature.initVerify(certs.get(0)) here despite it being the most obvious
// way to set it up, because we don't care about the constraints specified on the certificates: any
// cert that links a key to a domain name or other identity will do for us.
signature.initVerify(publicKey);
Protos.PaymentRequest.Builder reqToCheck = paymentRequest.toBuilder();
reqToCheck.setSignature(ByteString.EMPTY);
signature.update(reqToCheck.build().toByteArray());
if (!signature.verify(paymentRequest.getSignature().toByteArray()))
throw new PaymentRequestException.PkiVerificationException("Invalid signature, this payment request is not valid.");
// Signature verifies, get the names from the identity we just verified for presentation to the user.
X500Principal principal = certs.get(0).getSubjectX500Principal();
// At this point the Java crypto API falls flat on its face and dies - there's no clean way to get the
// different parts of the certificate name except for parsing the string. That's hard because of various
// custom escaping rules and the usual crap. So, use Bouncy Castle to re-parse the string into binary form
// again and then look for the names we want. Fail!
org.spongycastle.asn1.x500.X500Name name = new X500Name(principal.getName());
String entityName = null, orgName = null;
for (RDN rdn : name.getRDNs()) {
AttributeTypeAndValue pair = rdn.getFirst();
if (pair.getType().equals(RFC4519Style.cn))
entityName = ((ASN1String)pair.getValue()).getString();
else if (pair.getType().equals(RFC4519Style.o))
orgName = ((ASN1String)pair.getValue()).getString();
}
// Everything is peachy. Return some useful data to the caller.
PkiVerificationData data = new PkiVerificationData();
data.name = entityName;
data.orgName = orgName;
data.merchantSigningKey = publicKey;
data.rootAuthority = result.getTrustAnchor();
// Cache the result so we don't have to re-verify if this method is called again.
pkiVerificationData = data;
return data;
} catch (InvalidProtocolBufferException e) {
// Data structures are malformed.
throw new PaymentRequestException.InvalidPkiData(e);
} catch (CertificateException e) {
// The X.509 certificate data didn't parse correctly.
throw new PaymentRequestException.PkiVerificationException(e);
} catch (NoSuchAlgorithmException e) {
// Should never happen so don't make users have to think about it. PKIX is always present.
throw new RuntimeException(e);
} catch (InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
} catch (CertPathValidatorException e) {
// The certificate chain isn't known or trusted, probably, the server is using an SSL root we don't
// know about and the user needs to upgrade to a new version of the software (or import a root cert).
throw new PaymentRequestException.PkiVerificationException(e);
} catch (InvalidKeyException e) {
// Shouldn't happen if the certs verified correctly.
throw new PaymentRequestException.PkiVerificationException(e);
} catch (SignatureException e) {
// Something went wrong during hashing (yes, despite the name, this does not mean the sig was invalid).
throw new PaymentRequestException.PkiVerificationException(e);
} catch (IOException e) {
throw new PaymentRequestException.PkiVerificationException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
}
private KeyStore createKeyStore(@Nullable String path)
throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
String keyStoreType = KeyStore.getDefaultType();
char[] defaultPassword = "changeit".toCharArray();
if (path != null) {
// If the user provided path, only try to load the keystore at that path.
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
FileInputStream is = new FileInputStream(path);
keyStore.load(is, defaultPassword);
return keyStore;
}
path = System.getProperty("javax.net.ssl.trustStore");
if (path == null) {
// Check if we are on Android.
try {
Class Build = Class.forName("android.os.Build");
Object version = Build.getDeclaredField("VERSION").get(Build);
// Build.VERSION_CODES.ICE_CREAM_SANDWICH is 14.
if (version.getClass().getDeclaredField("SDK_INT").getInt(version) >= 14) {
// After ICS, Android provided this nice method for loading the keystore,
// so we don't have to specify the location explicitly.
KeyStore keystore = KeyStore.getInstance("AndroidCAStore");
keystore.load(null, null);
return keystore;
} else {
keyStoreType = "BKS";
path = System.getProperty("java.home") + "/etc/security/cacerts.bks".replace('/', File.separatorChar);
}
} catch (ClassNotFoundException e) {
// NOP. android.os.Build is not present, so we are not on Android.
} catch (NoSuchFieldException e) {
// This should never happen.
} catch (IllegalAccessException e) {
// This should never happen.
}
}
if (path == null) {
// We are not on Android. Try this default system location for Linux/Windows/OSX.
path = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar);
}
try {
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
FileInputStream is = new FileInputStream(path);
keyStore.load(is, defaultPassword);
return keyStore;
} catch (FileNotFoundException e) {
// If we failed to find a system trust store, load our own fallback trust store.
KeyStore keyStore = KeyStore.getInstance("JKS");
InputStream is = getClass().getResourceAsStream("cacerts");
keyStore.load(is, defaultPassword);
return keyStore;
}
}
private void parsePaymentRequest(Protos.PaymentRequest request) throws PaymentRequestException {
try {
if (request == null)
throw new PaymentRequestException("request cannot be null");
if (!request.hasPaymentDetailsVersion())
throw new PaymentRequestException.InvalidVersion("No version");
if (request.getPaymentDetailsVersion() != 1)
throw new PaymentRequestException.InvalidVersion("Version 1 required. Received version " + request.getPaymentDetailsVersion());
paymentRequest = request;
if (!request.hasSerializedPaymentDetails())
throw new PaymentRequestException("No PaymentDetails");
paymentDetails = Protos.PaymentDetails.newBuilder().mergeFrom(request.getSerializedPaymentDetails()).build();
if (paymentDetails == null)
throw new PaymentRequestException("Invalid PaymentDetails");
if (!paymentDetails.hasNetwork())
params = MainNetParams.get();
else
params = NetworkParameters.fromPmtProtocolID(paymentDetails.getNetwork());
if (params == null)
throw new PaymentRequestException.InvalidNetwork("Invalid network " + paymentDetails.getNetwork());
if (paymentDetails.getOutputsCount() < 1)
throw new PaymentRequestException.InvalidOutputs("No outputs");
for (Protos.Output output : paymentDetails.getOutputsList()) {
if (output.hasAmount())
totalValue = totalValue.add(BigInteger.valueOf(output.getAmount()));
}
// This won't ever happen in practice. It would only happen if the user provided outputs
// that are obviously invalid. Still, we don't want to silently overflow.
if (totalValue.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0)
throw new PaymentRequestException.InvalidOutputs("The outputs are way too big.");
} catch (InvalidProtocolBufferException e) {
throw new PaymentRequestException(e);
}
}
}

View File

@ -86,6 +86,7 @@ public class BitcoinURI {
public static final String FIELD_LABEL = "label";
public static final String FIELD_AMOUNT = "amount";
public static final String FIELD_ADDRESS = "address";
public static final String FIELD_PAYMENT_REQUEST_URL = "r";
public static final String BITCOIN_SCHEME = "bitcoin";
private static final String ENCODED_SPACE_CHARACTER = "%20";
@ -268,6 +269,14 @@ public class BitcoinURI {
public String getMessage() {
return (String) parameterMap.get(FIELD_MESSAGE);
}
/**
* @return The URL where a payment request (as specified in BIP 70) may
* be fetched.
*/
public String getPaymentRequestUrl() {
return (String) parameterMap.get(FIELD_PAYMENT_REQUEST_URL);
}
/**
* @param name The name of the parameter

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
/** 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.
*/
/*
* Authors: Mike Hearn, Gavin Andresen
*/
/* Notes:
* - Endianness: All byte arrays that represent numbers (such as hashes and private keys) are Big Endian
* - To regenerate after editing, run mvn clean package -DupdateProtobuf
*/
//
// Simple Bitcoin Payment Protocol messages
//
// Use fields 100+ for extensions;
// to avoid conflicts, register extensions at:
// https://en.bitcoin.it/wiki/Payment_Request
//
package payments;
option java_package = "org.bitcoin.protocols.payments";
option java_outer_classname = "Protos";
// Generalized form of "send payment to this/these bitcoin addresses"
message Output {
optional uint64 amount = 1 [default = 0]; // amount is integer-number-of-satoshis
required bytes script = 2; // usually one of the standard Script forms
}
message PaymentDetails {
optional string network = 1 [default = "main"]; // "main" or "test"
repeated Output outputs = 2; // Where payment should be sent
required uint64 time = 3; // Timestamp; when payment request created
optional uint64 expires = 4; // Timestamp; when this request should be considered invalid
optional string memo = 5; // Human-readable description of request for the customer
optional string payment_url = 6; // URL to send Payment and get PaymentACK
optional bytes merchant_data = 7; // Arbitrary data to include in the Payment message
}
message PaymentRequest {
optional uint32 payment_details_version = 1 [default = 1];
optional string pki_type = 2 [default = "none"]; // none / x509+sha256 / x509+sha1
optional bytes pki_data = 3; // depends on pki_type
required bytes serialized_payment_details = 4; // PaymentDetails
optional bytes signature = 5; // pki-dependent signature
}
message X509Certificates {
repeated bytes certificate = 1; // DER-encoded X.509 certificate chain
}
message Payment {
optional bytes merchant_data = 1; // From PaymentDetails.merchant_data
repeated bytes transactions = 2; // Signed transactions that satisfy PaymentDetails.outputs
repeated Output refund_to = 3; // Where to send refunds, if a refund is necessary
optional string memo = 4; // Human-readable message for the merchant
}
message PaymentACK {
required Payment payment = 1; // Payment message that triggered this ACK
optional string memo = 2; // human-readable message for customer
}

View File

@ -0,0 +1,191 @@
/**
* 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 com.google.bitcoin.protocols.payments;
import com.google.bitcoin.core.*;
import com.google.bitcoin.params.TestNet3Params;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoin.protocols.payments.Protos;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import static org.junit.Assert.*;
public class PaymentSessionTest {
private static final Logger log = LoggerFactory.getLogger(PaymentSessionTest.class);
private static final NetworkParameters params = TestNet3Params.get();
private static final String simplePaymentUrl = "http://a.simple.url.com/";
private static final String paymentRequestMemo = "send coinz noa plz kthx";
private static final String paymentMemo = "take ze coinz";
private static final ByteString merchantData = ByteString.copyFromUtf8("merchant data");
private static final long time = System.currentTimeMillis() / 1000L;
private ECKey serverKey;
private Transaction tx;
private TransactionOutput outputToMe;
BigInteger nanoCoins = Utils.toNanoCoins(1, 0);
@Before
public void setUp() throws Exception {
serverKey = new ECKey();
tx = new Transaction(params);
outputToMe = new TransactionOutput(params, tx, nanoCoins, serverKey);
tx.addOutput(outputToMe);
}
@Test
public void testSimplePayment() throws Exception {
// Create a PaymentRequest and make sure the correct values are parsed by the PaymentSession.
MockPaymentSession paymentSession = new MockPaymentSession(newSimplePaymentRequest());
assertEquals(paymentRequestMemo, paymentSession.getMemo());
assertEquals(nanoCoins, paymentSession.getValue());
assertEquals(simplePaymentUrl, paymentSession.getPaymentUrl());
assertTrue(new Date(time * 1000L).equals(paymentSession.getDate()));
assertTrue(paymentSession.getSendRequest().tx.equals(tx));
assertFalse(paymentSession.isExpired());
// Send the payment and verify that the correct information is sent.
// Add a dummy input to tx so it is considered valid.
tx.addInput(new TransactionInput(params, tx, outputToMe.getScriptBytes()));
ArrayList<Transaction> txns = new ArrayList<Transaction>();
txns.add(tx);
Address refundAddr = new Address(params, serverKey.getPubKeyHash());
paymentSession.sendPayment(txns, refundAddr, paymentMemo);
assertEquals(1, paymentSession.getPaymentLog().size());
assertEquals(simplePaymentUrl, paymentSession.getPaymentLog().get(0).getUrl().toString());
Protos.Payment payment = paymentSession.getPaymentLog().get(0).getPayment();
assertEquals(paymentMemo, payment.getMemo());
assertEquals(merchantData, payment.getMerchantData());
assertEquals(1, payment.getRefundToCount());
assertEquals(nanoCoins.longValue(), payment.getRefundTo(0).getAmount());
TransactionOutput refundOutput = new TransactionOutput(params, null, nanoCoins, refundAddr);
ByteString refundScript = ByteString.copyFrom(refundOutput.getScriptBytes());
assertTrue(refundScript.equals(payment.getRefundTo(0).getScript()));
}
@Test
public void testExpiredPaymentRequest() throws Exception {
MockPaymentSession paymentSession = new MockPaymentSession(newExpiredPaymentRequest());
assertTrue(paymentSession.isExpired());
// Send the payment and verify that an exception is thrown.
// Add a dummy input to tx so it is considered valid.
tx.addInput(new TransactionInput(params, tx, outputToMe.getScriptBytes()));
ArrayList<Transaction> txns = new ArrayList<Transaction>();
txns.add(tx);
try {
paymentSession.sendPayment(txns, null, null);
} catch(PaymentRequestException.Expired e) {
assertEquals(0, paymentSession.getPaymentLog().size());
assertEquals(e.getMessage(), "PaymentRequest is expired");
return;
}
fail("Expected exception due to expired PaymentRequest");
}
@Test
public void testPkiVerification() throws Exception {
InputStream in = getClass().getResourceAsStream("pki_test.bitcoinpaymentrequest");
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder().mergeFrom(in).build();
MockPaymentSession paymentSession = new MockPaymentSession(paymentRequest);
PaymentSession.PkiVerificationData pkiData = paymentSession.verifyPki();
assertEquals("www.bitcoincore.org", pkiData.name);
}
private Protos.PaymentRequest newSimplePaymentRequest() {
Protos.Output.Builder outputBuilder = Protos.Output.newBuilder()
.setAmount(nanoCoins.longValue())
.setScript(ByteString.copyFrom(outputToMe.getScriptBytes()));
Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder()
.setNetwork("test")
.setTime(time)
.setPaymentUrl(simplePaymentUrl)
.addOutputs(outputBuilder)
.setMemo(paymentRequestMemo)
.setMerchantData(merchantData)
.build();
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder()
.setPaymentDetailsVersion(1)
.setPkiType("none")
.setSerializedPaymentDetails(paymentDetails.toByteString())
.build();
return paymentRequest;
}
private Protos.PaymentRequest newExpiredPaymentRequest() {
Protos.Output.Builder outputBuilder = Protos.Output.newBuilder()
.setAmount(nanoCoins.longValue())
.setScript(ByteString.copyFrom(outputToMe.getScriptBytes()));
Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder()
.setNetwork("test")
.setTime(time - 10)
.setExpires(time - 1)
.setPaymentUrl(simplePaymentUrl)
.addOutputs(outputBuilder)
.setMemo(paymentRequestMemo)
.setMerchantData(merchantData)
.build();
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder()
.setPaymentDetailsVersion(1)
.setPkiType("none")
.setSerializedPaymentDetails(paymentDetails.toByteString())
.build();
return paymentRequest ;
}
private class MockPaymentSession extends PaymentSession {
private ArrayList<PaymentLogItem> paymentLog = new ArrayList<PaymentLogItem>();
public MockPaymentSession(Protos.PaymentRequest request) throws PaymentRequestException {
super(request);
}
public ArrayList<PaymentLogItem> getPaymentLog() {
return paymentLog;
}
protected ListenableFuture<Ack> sendPayment(final URL url, final Protos.Payment payment) {
paymentLog.add(new PaymentLogItem(url, payment));
return null;
}
public class PaymentLogItem {
private final URL url;
private final Protos.Payment payment;
PaymentLogItem(final URL url, final Protos.Payment payment) {
this.url = url;
this.payment = payment;
}
public URL getUrl() {
return url;
}
public Protos.Payment getPayment() {
return payment;
}
}
}
}

View File

@ -23,10 +23,18 @@ import com.google.bitcoin.net.discovery.PeerDiscovery;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.RegTestParams;
import com.google.bitcoin.params.TestNet3Params;
import com.google.bitcoin.protocols.payments.PaymentRequestException;
import com.google.bitcoin.protocols.payments.PaymentSession;
import com.google.bitcoin.store.*;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.bitcoin.uri.BitcoinURIParseException;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
@ -72,6 +80,7 @@ public class WalletTool {
private static PeerDiscovery discovery;
private static ValidationMode mode;
private static String password;
private static org.bitcoin.protocols.payments.Protos.PaymentRequest paymentRequest;
public static class Condition {
public enum Type {
@ -199,6 +208,8 @@ public class WalletTool {
parser.accepts("offline");
parser.accepts("ignore-mandatory-extensions");
OptionSpec<String> passwordFlag = parser.accepts("password").withRequiredArg();
OptionSpec<String> paymentRequestLocation = parser.accepts("payment-request").withRequiredArg();
parser.accepts("no-pki");
options = parser.parse(args);
final String HELP_TEXT = Resources.toString(WalletTool.class.getResource("wallet-tool-help.txt"), Charsets.UTF_8);
@ -305,20 +316,26 @@ public class WalletTool {
case RESET: reset(); break;
case SYNC: syncChain(); break;
case SEND:
if (!options.has(outputFlag)) {
System.err.println("You must specify at least one --output=addr:value.");
if (options.has(paymentRequestLocation) && options.has(outputFlag)) {
System.err.println("--payment-request and --output cannot be used together.");
return;
} else if (options.has(outputFlag)) {
BigInteger fee = BigInteger.ZERO;
if (options.has("fee")) {
fee = Utils.toNanoCoins((String)options.valueOf("fee"));
}
String lockTime = null;
if (options.has("locktime")) {
lockTime = (String) options.valueOf("locktime");
}
boolean allowUnconfirmed = options.has("allow-unconfirmed");
send(outputFlag.values(options), fee, lockTime, allowUnconfirmed);
} else if (options.has(paymentRequestLocation)) {
sendPaymentRequest(paymentRequestLocation.value(options), !options.has("no-pki"));
} else {
System.err.println("You must specify a --payment-request or at least one --output=addr:value.");
return;
}
BigInteger fee = BigInteger.ZERO;
if (options.has("fee")) {
fee = Utils.toNanoCoins((String)options.valueOf("fee"));
}
String lockTime = null;
if (options.has("locktime")) {
lockTime = (String) options.valueOf("locktime");
}
boolean allowUnconfirmed = options.has("allow-unconfirmed");
send(outputFlag.values(options), fee, lockTime, allowUnconfirmed);
break;
}
@ -461,6 +478,85 @@ public class WalletTool {
}
}
private static void sendPaymentRequest(String location, boolean verifyPki) {
if (location.startsWith("http")) {
try {
ListenableFuture<PaymentSession> future = PaymentSession.createFromUrl(location, verifyPki);
Futures.addCallback(future, new FutureCallback<PaymentSession>() {
@Override
public void onSuccess(PaymentSession session) {
if (session != null)
send(session);
else {
System.err.println("Server returned null session");
System.exit(1);
}
}
public void onFailure(Throwable thrown) {
System.err.println("Failed to fetch payment request " + thrown.getMessage());
System.exit(1);
}
});
} catch (PaymentRequestException e) {
System.err.println("Error creating payment session " + e.getMessage());
System.exit(1);
}
} else if (location.startsWith("bitcoin")) {
BitcoinURI paymentRequestURI = null;
try {
paymentRequestURI = new BitcoinURI(location);
} catch (BitcoinURIParseException e) {
System.err.println("Invalid bitcoin uri " + e.getMessage());
System.exit(1);
}
try {
ListenableFuture<PaymentSession> future = PaymentSession.createFromBitcoinUri(paymentRequestURI, verifyPki);
Futures.addCallback(future, new FutureCallback<PaymentSession>() {
@Override
public void onSuccess(PaymentSession session) {
if (session != null)
send(session);
else {
System.err.println("Server returned null session");
System.exit(1);
}
}
public void onFailure(Throwable thrown) {
System.err.println("Failed to fetch payment request " + thrown.getMessage());
System.exit(1);
}
});
} catch (PaymentRequestException e) {
System.err.println("Error creating payment session " + e.getMessage());
System.exit(1);
}
} else {
// Try to open the payment request as a file.
FileInputStream stream = null;
try {
File paymentRequestFile = new File(location);
stream = new FileInputStream(paymentRequestFile);
} catch (Exception e) {
System.err.println("Failed to open file " + e.getMessage());
System.exit(1);
}
try {
paymentRequest = org.bitcoin.protocols.payments.Protos.PaymentRequest.newBuilder().mergeFrom(stream).build();
} catch(IOException e) {
System.err.println("Failed to parse payment request from file " + e.getMessage());
System.exit(1);
}
PaymentSession session = null;
try {
session = new PaymentSession(paymentRequest, verifyPki);
} catch (PaymentRequestException e) {
System.err.println("Error creating payment session " + e.getMessage());
System.exit(1);
}
send(session);
}
}
private static void wait(WaitForEnum waitFor) throws BlockStoreException {
final CountDownLatch latch = new CountDownLatch(1);
setup();
@ -737,4 +833,49 @@ public class WalletTool {
setup();
System.out.println(wallet.toString(true, true, true, chain));
}
private static void send(PaymentSession session) {
try {
System.out.println("Payment Request");
System.out.println("Amount: " + session.getValue().doubleValue() / 100000 + "mBTC");
System.out.println("Date: " + session.getDate());
System.out.println("Memo: " + session.getMemo());
if (session.pkiVerificationData != null) {
System.out.println("Pki-Verified Name: " + session.pkiVerificationData.name);
if (session.pkiVerificationData.orgName != null)
System.out.println("Pki-Verified Org: " + session.pkiVerificationData.orgName);
}
final Wallet.SendRequest req = session.getSendRequest();
wallet.completeTx(req); // may throw InsufficientMoneyException.
// No refund address specified, no user-specified memo field.
ListenableFuture<PaymentSession.Ack> future = session.sendPayment(ImmutableList.of(req.tx), null, null);
Futures.addCallback(future, new FutureCallback<PaymentSession.Ack>() {
@Override
public void onSuccess(PaymentSession.Ack ack) {
try {
wallet.commitTx(req.tx);
System.out.println(ack.getMemo());
} catch (VerificationException e) {
System.err.println("Failed to send tx " + e.getMessage());
System.exit(1);
}
}
public void onFailure(Throwable thrown) {
System.err.println("Failed to send payment " + thrown.getMessage());
System.exit(1);
}
});
}catch (PaymentRequestException e) {
System.err.println("Failed to send payment " + e.getMessage());
System.exit(1);
} catch (VerificationException e) {
System.err.println("Failed to send payment " + e.getMessage());
System.exit(1);
} catch (IOException e) {
System.err.println("Invalid payment " + e.getMessage());
System.exit(1);
} catch (InsufficientMoneyException e) {
System.err.println("Insufficient funds: have " + Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
}
}

View File

@ -19,18 +19,28 @@ Usage: wallet-tool --flags action-name
sync Sync the wallet with the latest block chain (download new transactions).
If the chain file does not exist this will RESET the wallet.
reset Deletes all transactions from the wallet, for if you want to replay the chain.
send Creates a transaction with the given --output from this wallet and broadcasts, eg:
send Creates and broadcasts a transaction from the given wallet.
Requires either --output or --payment-request to be specified.
If --output is specified, a transaction is created from the provided output
from this wallet and broadcasted, eg:
--output=1GthXFQMktFLWdh5EPNGqbq3H6WdG8zsWj:1.245
You can repeat --output=address:value multiple times.
If the output destination starts with 04 and is 65 or 33 bytes long it will be
treated as a public key instead of an address and the send will use
<key> CHECKSIG as the script.
If --payment-request is specified, a transaction will be created using the provided
payment request. A payment request can be a local file, a bitcoin uri, or url to
download the payment request, e.g.:
--payment-request=/path/to/my.bitcoinpaymentrequest
--payment-request=bitcoin:?r=http://merchant.com/pay.php?123
--payment-request=http://merchant.com/pay.php?123
Other options include:
--fee=0.01 sets the tx fee
--locktime=1234 sets the lock time to block 1234
--locktime=2013/01/01 sets the lock time to 1st Jan 2013
--allow-unconfirmed will let you create spends of pending non-change outputs.
--no-pki disables pki verification for payment requests.
>>> GENERAL OPTIONS
--debuglog Enables logging from the core library.