Package lighthouse.protocol

Source Code of lighthouse.protocol.Project

package lighthouse.protocol;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import lighthouse.files.DiskManager;
import lighthouse.wallet.PledgingWallet;
import org.bitcoin.protocols.payments.Protos;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.protocols.payments.PaymentProtocolException;
import org.bitcoinj.protocols.payments.PaymentSession;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.DefaultRiskAnalysis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.io.Streams;

import javax.annotation.Nullable;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.time.Instant;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.time.Instant.now;
import static java.util.stream.Collectors.toList;
import static lighthouse.protocol.LHUtils.unchecked;

/**
* A Project represents something to which pledges can be made. It is serialized using an extended form of the BIP 70
* PaymentRequest message and typically stored on disk. Given a set of pledges and a TransactionBroadcaster, a Project
* can create a transaction that commits the payments. This class is immutable and not concerned with the storage of
* pledges. It is not a builder or holder for UI state, look at ProjectModel for that.
*/
public class Project {
    private final Logger log = LoggerFactory.getLogger(Project.class);
    private final NetworkParameters params;
    private final LHProtos.ProjectDetails projectReq;
    // These fields should be immutable pure functions of projectReq, as we may hand back projectReq later and
    // expect that it's not been changed.
    private final ImmutableList<TransactionOutput> outputs;
    private final long goalAmount, minPledgeAmount;
    private final String title;
    @Nullable private final URI  url;
    // Projects are identified by the hash of their serialized contents. There is no canonical encoding
    // and this ID is mostly used just as a key in various maps.
    private final Sha256Hash hash;

    private final byte[] authKey;
    private final int authKeyIndex;

    public Project(LHProtos.ProjectDetails details) throws PaymentProtocolException, InvalidProtocolBufferException {
        this(wrapDetails(details).build());
    }

    private static LHProtos.Project.Builder wrapDetails(LHProtos.ProjectDetails details) {
        LHProtos.Project.Builder project = LHProtos.Project.newBuilder();
        project.setSerializedPaymentDetails(details.toByteString());
        return project;
    }

    public Project(LHProtos.Project proto) throws PaymentProtocolException, InvalidProtocolBufferException {
        hash = Sha256Hash.create(proto.toByteArray());
        // "Cast" it to a regular BIP70 payment request, possibly losing data along the way, but that's OK
        // because we only want to do this to reuse the existing APIs.
        Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.parseFrom(proto.toByteString());
        PaymentSession session = new PaymentSession(paymentRequest, false);
        this.outputs = ImmutableList.copyOf(session.getSendRequest().tx.getOutputs());
        this.params = session.getNetworkParameters();
        this.projectReq = LHProtos.ProjectDetails.parseFrom(proto.getSerializedPaymentDetails());
        this.goalAmount = this.projectReq.getOutputsList().stream().mapToLong(LHProtos.Output::getAmount).sum();
        this.minPledgeAmount = this.projectReq.getExtraDetails().getMinPledgeSize();
        if (this.goalAmount <= 0)
            throw new Ex.ValueMismatch(this.goalAmount);
        this.title = this.projectReq.getExtraDetails().getTitle();
        if (this.projectReq.hasPaymentUrl()) {
            try {
                url = new URI(this.projectReq.getPaymentUrl());
                if (url.getHost() == null)
                    throw new Exception();
            } catch (Exception e) {
                throw new PaymentProtocolException("Invalid URL: " + this.projectReq.getPaymentUrl());
            }
        } else {
            url = null;
        }

        if (this.projectReq.hasMerchantData()) {
            LHProtos.OwnerData ownerData = LHProtos.OwnerData.parseFrom(this.projectReq.getMerchantData());
            this.authKeyIndex = ownerData.getAuthKeyIndex();
        } else {
            this.authKeyIndex = -1;
        }
        this.authKey = this.projectReq.getExtraDetails().getAuthKey().toByteArray();
    }

    public static LHProtos.ProjectDetails.Builder makeDetails(String title, String memo, Address to, Coin value, DeterministicKey authKey, int lookaheadSize) {
        LHProtos.ProjectDetails.Builder details = LHProtos.ProjectDetails.newBuilder();
        final long now = Utils.currentTimeSeconds();
        final long oneMonthFromNow = now + (86400 * 30);
        details.setTime(now);
        details.setExpires(oneMonthFromNow);
        details.getExtraDetailsBuilder().setTitle(title).setAuthKey(ByteString.copyFrom(authKey.getPubKey()));
        if (authKey.getChildNumber().num() > lookaheadSize) {
            LHProtos.OwnerData ownerData = LHProtos.OwnerData.newBuilder().setAuthKeyIndex(authKey.getChildNumber().num()).build();
            details.setMerchantData(ownerData.toByteString());
        }
        details.setMemo(memo);
        details.setNetwork("test");
        LHProtos.Output.Builder output = details.addOutputsBuilder();
        output.setAmount(value.value);
        output.setScript(ByteString.copyFrom(ScriptBuilder.createOutputScript(to).getProgram()));
        return details;
    }

    /** Just returns a project that wraps the serialized details from this model. */
    public LHProtos.Project getProto() {
        LHProtos.Project.Builder proto = LHProtos.Project.newBuilder();
        proto.setSerializedPaymentDetails(projectReq.toByteString());
        return proto.build();
    }

    public LHProtos.ProjectDetails getProtoDetails() {
        return projectReq;
    }

    /**
     * Returns a string that is used to uniquely identify a project: a 256-bit hash of the project contents.
     */
    public String getID() {
        return hash.toString();
    }

    /** Returns a human-readable title for the project. */
    public String getTitle() {
        return title;
    }

    /** Returns a human-readable description for the project. */
    public String getMemo() {
        return projectReq.getMemo();
    }

    /** Returns the total target value of the crowdfund in satoshis (i.e. sum of all contract outputs). */
    public Coin getGoalAmount() {
        return Coin.valueOf(goalAmount);
    }

    /** Returns a byte array containing a JPEG or PNG to use for the cover image. */
    public ByteString getCoverImage() {
        return projectReq.getExtraDetails().getCoverImage();
    }

    /** Returns a deep copy of the list of outputs. */
    public List<TransactionOutput> getOutputs() {
        return outputs.stream().map(TransactionOutput::duplicateDetached).collect(toList());
    }

    /** Returns expiry time in seconds since the epoch */
    public Instant getExpires() {
        return Instant.ofEpochSecond(projectReq.getExpires());
    }

    /** If true, do not allow any further pledges to be made to this project. */
    public boolean isExpired() {
        return now().isAfter(getExpires());
    }

    /** Returns the URL (if any) to which the pledge should be submitted, or null if none specified. */
    @Nullable
    public URI getPaymentURL() {
        if (url != null && "localhost".equals(url.getHost())) {
            // Switch port for easier local testing (vs the default of port 80).
            URI newUrl = unchecked(() -> new URI(String.format("http://%s:%d%s", url.getHost(), LHUtils.HTTP_LOCAL_TEST_PORT, url.getPath())));
            log.info("Switched URL to {}", newUrl);
            return newUrl;
        } else {
            return url;
        }
    }

    /**
     * Called to check a pledge message that was submitted by a pledger. The returned future completes once UTXO lookup
     * and script execution is done, or an error is encountered in which case the future will have the Ex subclass
     * representing the error.
     */
    public CompletableFuture<LHProtos.Pledge> verifyPledge(UTXOSource peer, LHProtos.Pledge pledge) {
        try {
            log.info("Checking pledge for project '{}' [{}]", getTitle(), getID());
            Transaction tx = fastSanityCheck(pledge);
            return lookupUTXOs(peer, tx).thenApply((result) -> {
                if (result.size() != tx.getInputs().size()) {
                    log.error("Could not locate all pledge UTXOs: may be double spent. Found:\n{}\n ... " +
                            " and pledge tx is...\n{}", result.toString(), tx);
                    throw new Ex.UnknownUTXO();
                }
                // The pledge matches some unspent outputs: now verify the scripts can spend and are signed with
                // SIGHASH_ANYONECANPAY as appropriate.
                verifyScripts(tx, result);
                // Check that the pledge.total_input_value field is consistent/correct, and is not under the min
                // pledge size for this project.
                verifyValues(pledge, result);
                // The pledge appears to be connected to unspent outputs and should be accepted by the network.
                // So we think it's a success!
                log.info("Pledge appears to be OK");
                return pledge;
            });
        } catch (Exception e) {
            CompletableFuture<LHProtos.Pledge> future = new CompletableFuture<>();
            future.completeExceptionally(e);
            return future;
        }
    }

    private void verifyValues(LHProtos.Pledge pledge, List<TransactionOutput> result) {
        // Verify that the pledge total_input_value field is consistent with the fetched UTXOs.
        long totalValue = 0;
        for (TransactionOutput output : result) totalValue += output.getValue().longValue();
        if (pledge.getTotalInputValue() != totalValue || totalValue == 0)
            throw new Ex.CachedValueMismatch();
        if (totalValue < minPledgeAmount)
            throw new Ex.PledgeTooSmall(minPledgeAmount - totalValue);
    }

    private void verifyScripts(Transaction tx, List<TransactionOutput> result) throws VerificationException {
        // We assume that the ordering of the result list matches the tx input order.
        //
        // Add a random input that cannot be predicted by the pledgor, and run the script. If all the
        // signatures are SIGHASH_ANYONECANPAY and the input scripts satisfy the output scripts, the
        // random input should be ignored and verification should pass.
        //
        checkState(tx.getInputs().size() == result.size());
        tx = addRandomInput(tx);
        for (int i = 0; i < tx.getInputs().size() - 1; i++) {
            // Also check that the script we're about to run is of an expected form and contains a CHECKSIG. A remote
            // peer could have provided us with an OP_TRUE script or equivalent that would always pass here, even if
            // that's not the real script the tx was signed with. Forcing it to contain a CHECKSIG ensures that the
            // signature in the input is verified and that can't pass unless the scriptPubKey is as expected.
            // We also allow P2SH here because the input script must contain the correct output script in this case,
            // thus the provided output must match.
            final Script scriptPubKey = result.get(i).getScriptPubKey();
            if (isSafeToCrossCheck(scriptPubKey)) {
                TransactionInput input = tx.getInput(i);
                // Try to stop some idiot/troll from giving us a non-standard input, thus making us think we've raised
                // our funds but actually cannot easily claim the money.
                DefaultRiskAnalysis.RuleViolation violation = isInputStandard(input, scriptPubKey);
                if (violation != DefaultRiskAnalysis.RuleViolation.NONE) {
                    log.error("TX input {} is non-standard due to rule {}", violation);
                    throw new Ex.NonStandardInput();
                }
                try {
                    input.verify(result.get(i));
                } catch (VerificationException e) {
                    log.error("TX input {} failed with scriptSig {}    scriptPubKey {}", i, input.getScriptSig(), result.get(0).getScriptPubKey());
                    throw e;
                }
            } else
                throw new VerificationException("Unexpected script form returned by peer: " + scriptPubKey);
        }
    }

    // TODO: Move this into bitcoinj post-0.12
    private DefaultRiskAnalysis.RuleViolation isInputStandard(TransactionInput input, Script scriptPubKey) {
        DefaultRiskAnalysis.RuleViolation violation = input.isStandard();
        if (violation != DefaultRiskAnalysis.RuleViolation.NONE)
            return violation;
        Script scriptSig = input.getScriptSig();
        int args = getNumExpectedScriptSigArgs(scriptPubKey);
        LinkedList<byte[]> stack = Lists.newLinkedList();
        // This is fast and cannot execute any signature checks, because we already verified with the previous
        // isStandard() call that it only contains data pushes.
        Script.executeScript(null, -1, scriptSig, stack, true);
        if (scriptPubKey.isPayToScriptHash()) {
            Script redeemScript = null;
            try {
                if (stack.isEmpty())
                    throw new VerificationException("Empty stack");   // Should never happen: can't have empty scripts.
                redeemScript = new Script(stack.getLast());
                args += getNumExpectedScriptSigArgs(redeemScript);
            } catch (VerificationException e) {
                // We can get here if the redeem script is corrupted or unparseable in some way, or if the script type
                // simply isn't recognised by getNumExpected... - Bitcoin Core appears to simply treat any garbage
                // script as non-standard.
                //
                // Regardless, a non-recognised script is always treated as OK if it has 15 or fewer sigops, even if
                // it were to leave extra stuff on the stack.
                if (redeemScript != null && Script.getSigOpCount(stack.getLast()) <= Script.MAX_P2SH_SIGOPS)
                    return DefaultRiskAnalysis.RuleViolation.NONE;
            }
        }
        if (stack.size() != args)
            return DefaultRiskAnalysis.RuleViolation.NONEMPTY_STACK;
        return DefaultRiskAnalysis.RuleViolation.NONE;
    }

    private int getNumExpectedScriptSigArgs(Script scriptPubKey) {
        if (scriptPubKey.isSentToAddress())
            return 2;
        else if (scriptPubKey.isSentToRawPubKey())
            return 1;
        else if (scriptPubKey.isSentToMultiSig())
            return scriptPubKey.getChunks().get(0).decodeOpN();
        else if (scriptPubKey.isPayToScriptHash())
            return 1;   // Doesn't include the args needed by the script itself.
        else
            throw new VerificationException("Unknown scriptPubKey type");
    }

    private boolean isSafeToCrossCheck(Script scriptPubKey) {
        return scriptPubKey.isSentToRawPubKey() ||
               scriptPubKey.isSentToAddress() ||
               scriptPubKey.isSentToMultiSig() ||
               scriptPubKey.isPayToScriptHash();
    }

    private Transaction addRandomInput(Transaction tx) {
        tx = new Transaction(params, tx.bitcoinSerialize());
        byte[] rand = new byte[32];
        new SecureRandom().nextBytes(rand);
        tx.addInput(new TransactionInput(params, tx, new ScriptBuilder().data(rand).build().getProgram()));
        return tx;
    }

    private CompletableFuture<List<TransactionOutput>> lookupUTXOs(UTXOSource peer, Transaction tx) {
        List<TransactionOutPoint> outPoints = tx.getInputs().stream().map(TransactionInput::getOutpoint)
                .collect(toList());
        return peer.getUTXOs(outPoints);
    }

    /** Check that the pledge protobuf passes basic validity checks. */
    public Transaction fastSanityCheck(LHProtos.Pledge pledge) {
        if (pledge.getTransactionsList().isEmpty())
            throw new Ex.NoTransactionData();
        // We take the last the transaction because the others are dependencies.
        Transaction tx = LHUtils.pledgeToTx(params, pledge);
        if (tx.getOutputs().size() != outputs.size())
            throw new Ex.TxWrongNumberOfOutputs(tx.getOutputs().size(), outputs.size());
        // Output scripts must match project output scripts. We assume the project creator doesn't specify an invalid
        // or non-standard output: we could check for that later, however.
        for (int i = 0; i < tx.getOutputs().size(); i++) {
            if (!outputs.get(i).duplicateDetached().equals(tx.getOutput(i).duplicateDetached()))
                throw new Ex.OutputMismatch();
        }
        tx.verify();
        return tx;
    }

    /**
     * Returns a Transaction that combines the given pledges together. No fee is included.
     *
     * @throws lighthouse.protocol.Ex.ValueMismatch if the pledges don't total to an exact match for the goal.
     */
    public Transaction completeContract(Set<LHProtos.Pledge> pledges) {
        Transaction contract = new Transaction(params);
        outputs.forEach(contract::addOutput);
        long allPledgesValue = pledges.stream().mapToLong(LHProtos.Pledge::getTotalInputValue).sum();
        if (allPledgesValue != goalAmount)
            throw new Ex.ValueMismatch(allPledgesValue - goalAmount);
        pledges.stream().map(this::fastSanityCheck).forEach(pledge -> {
            pledge.getInputs().forEach(contract::addInput);
        });
        contract.setPurpose(Transaction.Purpose.ASSURANCE_CONTRACT_CLAIM);
        contract.verify();
        return contract;
    }

    /** Returns a future for the project status that completes when successfully downloaded via HTTP. */
    public CompletableFuture<LHProtos.ProjectStatus> getStatus(PledgingWallet wallet, @Nullable KeyParameter key) {
        final URI paymentURL = getPaymentURL();
        if (paymentURL == null)
            return null;
        CompletableFuture<LHProtos.ProjectStatus> future = new CompletableFuture<>();
        Thread thread = new Thread(() -> {
            try {
                URLConnection connection = getServerQueryURL(wallet, key).openConnection();
                connection.setDoOutput(true);
                connection.setConnectTimeout(10 * 1000);
                connection.addRequestProperty("User-Agent", "Lighthouse/1.0");
                connection.connect();
                byte[] bits = Streams.readAllLimited(connection.getInputStream(), 1024 * 1024)// 1mb limit.
                LHProtos.ProjectStatus status = LHProtos.ProjectStatus.parseFrom(bits);
                future.complete(status);
            } catch (Exception e) {
                log.error("Failed download from server " + paymentURL, e);
                future.completeExceptionally(e);
            }
        }, "Project downloader");
        thread.setDaemon(true);
        thread.start();
        return future;
    }

    private URL getServerQueryURL(PledgingWallet wallet, @Nullable KeyParameter key) {
        // It's ludicrous that Java has two URI/URL classes and both of them suck. How hard can this be, people?!
        URI uri = checkNotNull(getPaymentURL(), "Not a server assisted project");
        String path = uri.getPath();
        // The actual message used doesn't really matter as we simply are using signatures like a password here.
        // Communication with the server absolutely should be protected by SSL, in which case we have confidentiality
        // so replay attacks aren't a concern.
        String msg = getID();
        String rawSig = signAsOwner(wallet, msg, key);
        if (rawSig == null)
            return unchecked(uri::toURL);
        String signature = unchecked(() -> URLEncoder.encode(rawSig, "UTF-8"));
        if (path.contains("?"))
            path = path + "&msg=" + msg + "&sig=" + signature;
        else
            path = path + "?msg=" + msg + "&sig=" + signature;
        String fpath = path;
        return unchecked(() -> new URL(uri.getScheme(), uri.getHost(), uri.getPort(), fpath));
    }

    @Override
    public String toString() {
        return String.format("%s [goal: %s]", getTitle(), getGoalAmount());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Project project = (Project) o;

        if (!projectReq.equals(project.projectReq)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return projectReq.hashCode();
    }

    public String getSuggestedFileName() {
        return getTitle() + DiskManager.PROJECT_FILE_EXTENSION;
    }

    public Coin getMinPledgeAmount() {
        return getMinPledgeAmountFrom(minPledgeAmount);
    }

    /**
     * Returns the max of the min pledge amount recorded in the project definition, and the min output value times two.
     * The reason is: we need to be able to revoke a pledge we made, and that may require paying a fee. If we allowed
     * a pledge of the dust amount, we'd be unable to revoke because the entire amount we're trying to revoke would
     * get consumed in fees.
     */
    public static Coin getMinPledgeAmountFrom(long minPledgeAmount) {
        return Coin.valueOf(Math.max(minPledgeAmount, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(4).value));
    }

    @Nullable
    public String signAsOwner(PledgingWallet wallet, String message, @Nullable KeyParameter aesKey) {
        DeterministicKey realKey = wallet.getAuthKeyFromIndexOrPubKey(authKey, authKeyIndex);
        if (realKey == null || (aesKey == null && realKey.isEncrypted()))
            return null;
        return realKey.signMessage(message, aesKey);
    }

    public void authenticateOwner(String message, String signatureBase64) throws SignatureException {
        ECKey.fromPublicOnly(authKey).verifyMessage(message, signatureBase64);
    }

    public byte[] getAuthKey() {
        return authKey;
    }

    public int getAuthKeyIndex() {
        return authKeyIndex;
    }

    public NetworkParameters getParams() {
        return params;
    }
}
TOP

Related Classes of lighthouse.protocol.Project

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.