From f6cd23d724f8fed886527dd731709191cc9e898d Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 16 Jan 2024 13:53:43 -0800 Subject: [PATCH] feat(coinjoin): improve asset lock handling and other fixes (#244) * feat: improve asset lock handling to match v20 updates * refactor: rename credit funding to asset lock * tests: update tests * fix(coinjoin): add missing is spent check to countInputsWithAmount * feat(wallet-tool): add parameter to return change with coinjoin send * feat(coinjoin): add tx history at the end of a wallet dump * refactor: improve AssetLockTransaction for multiple credit outputs * refactor: rename setAssetLockPublicKey to addAssetLockPublicKey * tests: add more tests to AuthenticationGroupExtensionTest * fix: fix incorrect inequality check in createDenominate * fixes bug that prevented creation of more denominations past the goal count * feat: add txid to the transaction history report --- .../coinjoin/CoinJoinClientSession.java | 2 +- .../coinjoin/CoinJoinSendRequest.java | 6 +- .../org/bitcoinj/core/BitcoinSerializer.java | 4 +- .../java/org/bitcoinj/core/Transaction.java | 24 +- .../bitcoinj/evolution/AssetLockPayload.java | 25 +- .../evolution/AssetLockTransaction.java | 247 ++++++++++++++++++ .../evolution/CreditFundingTransaction.java | 225 ---------------- ...=> AssetLockTransactionEventListener.java} | 8 +- .../main/java/org/bitcoinj/script/Script.java | 6 +- .../org/bitcoinj/script/ScriptBuilder.java | 4 +- .../org/bitcoinj/script/ScriptPattern.java | 17 +- .../java/org/bitcoinj/wallet/SendRequest.java | 14 +- .../main/java/org/bitcoinj/wallet/Wallet.java | 33 ++- .../java/org/bitcoinj/wallet/WalletEx.java | 62 ++++- .../AuthenticationGroupExtension.java | 66 ++--- .../org/bitcoinj/evolution/AssetLockTest.java | 24 ++ .../evolution/AssetLockTransactionTest.java | 198 ++++++++++++++ .../CreditFundingTransactionTest.java | 187 ------------- .../AuthenticationGroupExtensionTest.java | 101 ++++++- .../java/org/bitcoinj/tools/WalletTool.java | 12 +- 20 files changed, 753 insertions(+), 512 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java delete mode 100644 core/src/main/java/org/bitcoinj/evolution/CreditFundingTransaction.java rename core/src/main/java/org/bitcoinj/evolution/listeners/{CreditFundingTransactionEventListener.java => AssetLockTransactionEventListener.java} (77%) create mode 100644 core/src/test/java/org/bitcoinj/evolution/AssetLockTransactionTest.java delete mode 100644 core/src/test/java/org/bitcoinj/evolution/CreditFundingTransactionTest.java diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java index 9ecf248291..f5cf8c8cb1 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java @@ -314,7 +314,7 @@ public int process(Coin amount) { // Go big to small for (Coin denomValue : denoms) { - if (balanceToDenominate.isGreaterThanOrEqualTo(Coin.ZERO)) break; + if (balanceToDenominate.isLessThanOrEqualTo(Coin.ZERO)) break; int nOutputs = 0; // Number of denoms we can create given our denom and the amount of funds we have left diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinSendRequest.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinSendRequest.java index cf410746d6..3b90326ec4 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinSendRequest.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinSendRequest.java @@ -19,7 +19,7 @@ public static SendRequest to(Wallet wallet, Address destination, Coin value) { } /** - *

Creates a new SCoinJoin endRequest to the given pubkey for the given value.

+ *

Creates a new CoinJoin SendRequest to the given pubkey for the given value.

*/ public static SendRequest to(Wallet wallet, ECKey destination, Coin value) { SendRequest req = SendRequest.to(wallet.getParams(), destination, value); @@ -28,10 +28,10 @@ public static SendRequest to(Wallet wallet, ECKey destination, Coin value) { return req; } /** Simply wraps a pre-built incomplete CoinJoin transaction provided by you. */ - public static SendRequest forTx(Wallet wallet, Transaction tx) { + public static SendRequest forTx(Wallet wallet, Transaction tx, boolean returnChange) { SendRequest req = SendRequest.forTx(tx); req.coinSelector = new CoinJoinCoinSelector(wallet); - req.returnChange = false; + req.returnChange = returnChange; return req; } } diff --git a/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java b/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java index 164b764414..6228b64a85 100644 --- a/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java +++ b/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java @@ -19,7 +19,7 @@ import org.bitcoinj.coinjoin.*; import org.bitcoinj.crypto.BLSScheme; -import org.bitcoinj.evolution.CreditFundingTransaction; +import org.bitcoinj.evolution.AssetLockTransaction; import org.bitcoinj.evolution.GetSimplifiedMasternodeListDiff; import org.bitcoinj.evolution.SimplifiedMasternodeListDiff; import org.bitcoinj.governance.GovernanceObject; @@ -101,7 +101,7 @@ public class BitcoinSerializer extends MessageSerializer { names.put(SendHeadersMessage.class, "sendheaders"); names.put(SendAddressMessageV2.class, "sendaddrv2"); names.put(GetMasternodePaymentRequestSyncMessage.class, "mnget"); - names.put(CreditFundingTransaction.class, "tx"); + names.put(AssetLockTransaction.class, "tx"); names.put(GetQuorumRotationInfo.class, "getqrinfo"); names.put(QuorumRotationInfo.class, "qrinfo"); // CoinJoin diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index d4dadd6dca..47acde2038 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -761,10 +761,15 @@ public String toString(@Nullable AbstractBlockChain chain, @Nullable CharSequenc s.append('\n'); if (updatedAt != null) s.append(indent).append("updated: ").append(Utils.dateTimeFormat(updatedAt)).append('\n'); - if (version != MIN_STANDARD_VERSION) - s.append(indent).append("version ").append(version).append('\n'); - Type type = (getVersionShort() == SPECIAL_VERSION) ? getType() : Type.TRANSACTION_NORMAL; - s.append(" type ").append(type.toString()).append('(').append(type.getValue()).append(")\n"); + if (version != MIN_STANDARD_VERSION) { + if (getVersionShort() == SPECIAL_VERSION) { + s.append(indent).append("version: ").append(getVersionShort()).append('\n'); + Type type = (getVersionShort() == SPECIAL_VERSION) ? getType() : Type.TRANSACTION_NORMAL; + s.append(" type: ").append(type.toString()).append('(').append(type.getValue()).append(")\n"); + } else { + s.append(indent).append("version: ").append(version).append('\n'); + } + } if (isTimeLocked()) { s.append(indent).append("time locked until "); if (lockTime < LOCKTIME_THRESHOLD) { @@ -839,11 +844,10 @@ public String toString(@Nullable AbstractBlockChain chain, @Nullable CharSequenc s.append(indent).append(" "); ScriptType scriptType = scriptPubKey.getScriptType(); if (scriptType != null) { - if (scriptType != ScriptType.CREDITBURN) + if (scriptType != ScriptType.ASSETLOCK) s.append(scriptType).append(" addr:").append(scriptPubKey.getToAddress(params)); - else if (ScriptPattern.isCreditBurn(scriptPubKey)) { - byte [] hash160 = ScriptPattern.extractCreditBurnKeyId(scriptPubKey); - s.append(scriptType).append(" addr:").append(Address.fromPubKeyHash(params, hash160)); + else if (ScriptPattern.isAssetLock(scriptPubKey) && getType() == Type.TRANSACTION_ASSET_LOCK) { + s.append(scriptType); } } else s.append("unknown script type"); @@ -867,8 +871,8 @@ else if (ScriptPattern.isCreditBurn(scriptPubKey)) { s.append(indent).append(" fee ").append(fee.multiply(1000).divide(size).toFriendlyString()).append("/kB, ") .append(fee.toFriendlyString()).append(" for ").append(size).append(" bytes\n"); } - if (getVersionShort() == SPECIAL_VERSION && type.isSpecial()) - s.append(indent).append(" payload ").append(getExtraPayloadObject()).append('\n'); + if (getVersionShort() == SPECIAL_VERSION && getType().isSpecial()) + s.append(indent).append("payload: ").append(getExtraPayloadObject()).append('\n'); return s.toString(); } diff --git a/core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java b/core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java index 5bd5a866d0..dca1431853 100644 --- a/core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java +++ b/core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java @@ -18,17 +18,20 @@ import com.google.common.collect.Lists; +import org.bitcoinj.core.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.ProtocolException; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.VarInt; +import org.bitcoinj.script.Script; import org.json.JSONObject; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import static org.bitcoinj.core.Transaction.Type.TRANSACTION_ASSET_LOCK; @@ -79,8 +82,19 @@ public int getCurrentVersion() { } public String toString() { - return String.format("AssetLockPayload(creditOutputs: %d)", - creditOutputs.size()); + StringBuilder s = new StringBuilder("AssetLockPayload"); + creditOutputs.forEach(output -> { + Script scriptPubKey = output.getScriptPubKey(); + s.append("\n out "); + s.append(scriptPubKey.getChunks().size() > 0 ? scriptPubKey.toString() : ""); + s.append(" "); + s.append(output.getValue().toFriendlyString()); + s.append('\n'); + s.append(" "); + Script.ScriptType scriptType = scriptPubKey.getScriptType(); + s.append(scriptType).append(" addr:").append(scriptPubKey.getToAddress(params)); + }); + return s.toString(); } @Override @@ -97,6 +111,13 @@ public List getCreditOutputs() { return creditOutputs; } + public Coin getFundingAmount() { + return creditOutputs.stream() + .map(TransactionOutput::getValue) + .reduce(Coin.ZERO, Coin::add); + } + + @Override public JSONObject toJson() { JSONObject result = super.toJson(); diff --git a/core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java b/core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java new file mode 100644 index 0000000000..2bc79c71b0 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java @@ -0,0 +1,247 @@ +/* + * Copyright 2020 Dash Core Group + * + * 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.evolution; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.IDeterministicKey; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptPattern; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.TreeMap; + +import static com.google.common.base.Preconditions.checkState; + +/** + * This class extends Transaction and is used to create a funding + * transaction for an identity. It also can store other information + * that is not stored in the blockchain transaction which includes + * the public or private key's associated with this transaction. + */ +public class AssetLockTransaction extends Transaction { + + private ArrayList lockedOutpoints; + private ArrayList identityIds; + private TreeMap assetLockPublicKeys; + private ArrayList assetLockPublicKeyIds; + private AssetLockPayload assetLockPayload; + + + + public AssetLockTransaction(NetworkParameters params) { + super(params); + } + + /** + * Create an asset lock transaction from an existing transaction. + * This should only be called if {@link AssetLockTransaction#isAssetLockTransaction(Transaction)} + * returns true. + * @param tx this transaction should be a credit funding transaction + */ + public AssetLockTransaction(Transaction tx) { + super(tx.getParams(), tx.bitcoinSerialize(), 0); + } + + /** + * Creates a credit funding transaction with a single credit output. + * @param params + * @param assetLockPublicKey The key from which the hash160 will be placed in the OP_RETURN output + * @param fundingAmount The amount of dash that will be locked in the OP_RETURN output + */ + public AssetLockTransaction(NetworkParameters params, ECKey assetLockPublicKey, Coin fundingAmount) { + super(params); + setVersionAndType(SPECIAL_VERSION, Type.TRANSACTION_ASSET_LOCK); + this.assetLockPublicKeys = Maps.newTreeMap(); + assetLockPublicKeys.put(0, assetLockPublicKey); + this.assetLockPublicKeyIds = Lists.newArrayList(); + assetLockPublicKeyIds.add(KeyId.fromBytes(assetLockPublicKey.getPubKeyHash())); + this.identityIds = Lists.newArrayList(); + identityIds.add(Sha256Hash.ZERO_HASH); + + TransactionOutput realOutput = new TransactionOutput(params, this, fundingAmount, Address.fromKey(params, assetLockPublicKey)); + + lockedOutpoints = Lists.newArrayList(); + TransactionOutput assetLockOutput = new TransactionOutput(params, null, fundingAmount, ScriptBuilder.createAssetLockOutput().getProgram()); + assetLockPayload = new AssetLockPayload(params, Lists.newArrayList(realOutput)); + setExtraPayload(assetLockPayload); + addOutput(assetLockOutput); + } + + /** + * Creates a credit funding transaction by reading payload. + * Length of a transaction is fixed. + */ + + public AssetLockTransaction(NetworkParameters params, byte [] payload) { + super(params, payload, 0); + } + + /** + * Deserialize and initialize some fields from the credit burn output + */ + @Override + protected void parse() throws ProtocolException { + super.parse(); + parseTransaction(); + } + + @Override + protected void unCache() { + super.unCache(); + lockedOutpoints.clear(); + identityIds.clear(); + assetLockPublicKeyIds.clear(); + } + + /** + * Initializes lockedOutpoints and the hash160 + * assetlock key + */ + private void parseTransaction() { + assetLockPayload = (AssetLockPayload) getExtraPayloadObject(); + lockedOutpoints = Lists.newArrayList(); + assetLockPublicKeyIds = Lists.newArrayList(); + assetLockPublicKeys = Maps.newTreeMap(); + identityIds = Lists.newArrayList(); + getLockedOutpoint(); + getAssetLockPublicKeyId(); + getIdentityId(); + } + + /** + * Sets lockedOutput and returns output that has the OP_RETURN script + */ + + public TransactionOutput getLockedOutput() { + return getLockedOutput(0); + } + + public TransactionOutput getLockedOutput(int outputIndex) { + return assetLockPayload.getCreditOutputs().get(outputIndex); + } + + public TransactionOutPoint getLockedOutpoint() { + return getLockedOutpoint(0); + } + + public AssetLockPayload getAssetLockPayload() { + return assetLockPayload; + } + + /** + * Sets lockedOutpoint and returns outpoint that has the OP_RETURN script + */ + + + + public TransactionOutPoint getLockedOutpoint(int outputIndex) { + if (lockedOutpoints.isEmpty()) { + for (int i = 0; i < assetLockPayload.getCreditOutputs().size(); ++i) { + lockedOutpoints.add(new TransactionOutPoint(params, i, Sha256Hash.wrap(getTxId().getReversedBytes()))); + } + } + return lockedOutpoints.get(outputIndex); + } + + public Coin getFundingAmount() { + return assetLockPayload.getFundingAmount(); + } + + /** + * Returns the credit burn identifier, which is the sha256(sha256(outpoint)) + */ + public Sha256Hash getIdentityId() { + return getIdentityId(0); + } + + public Sha256Hash getIdentityId(int outputIndex) { + if(identityIds.isEmpty()) { + assetLockPayload.getCreditOutputs().forEach(transactionOutput -> { + try { + ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(36); + getLockedOutpoint(outputIndex).bitcoinSerialize(bos); + identityIds.add(Sha256Hash.twiceOf(bos.toByteArray())); + + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + return identityIds.get(0); + } + + public ECKey getAssetLockPublicKey() { + return getAssetLockPublicKey(0); + } + + public ECKey getAssetLockPublicKey(int outputIndex) { + return assetLockPublicKeys.get(outputIndex); + } + + + public KeyId getAssetLockPublicKeyId() { + return getAssetLockPublicKeyId(0); + } + public KeyId getAssetLockPublicKeyId(int outputIndex) { + if(assetLockPublicKeyIds.isEmpty()) { + assetLockPayload.getCreditOutputs().forEach(transactionOutput -> assetLockPublicKeyIds.add(KeyId.fromBytes(ScriptPattern.extractHashFromP2PKH(assetLockPayload.getCreditOutputs().get(0).getScriptPubKey())))); + } + return assetLockPublicKeyIds.get(outputIndex); + } + + public int getUsedDerivationPathIndex(int outputIndex) { + ECKey key = getAssetLockPublicKey(0); + if (key instanceof IDeterministicKey) { + IDeterministicKey deterministicKey = (IDeterministicKey) key; + return deterministicKey.getPath().get(deterministicKey.getDepth() - 1).num(); + } + return -1; + } + + public void addAssetLockPublicKey(ECKey assetLockPublicKey) { + int index = assetLockPublicKeyIds.indexOf(KeyId.fromBytes(assetLockPublicKey.getPubKeyHash())); + checkState(index != -1, "cannot find public key hash for " + assetLockPublicKey); + assetLockPublicKeys.put(index, assetLockPublicKey); + } + + /** + * Determines if a transaction has one or more credit burn outputs + * and therefore is a is credit funding transaction + */ + public static boolean isAssetLockTransaction(Transaction tx) { + return tx.getVersionShort() == SPECIAL_VERSION && tx.getType() == Type.TRANSACTION_ASSET_LOCK && + tx.getOutputs().stream().anyMatch(output -> ScriptPattern.isAssetLock(output.getScriptPubKey())); + } + + /** + * Determines the first output that is a credit burn output + * or returns -1. + */ + public long getAssetLockOutputIndex() { + int outputCount = getOutputs().size(); + for (int i = 0; i < outputCount; ++i) { + if (ScriptPattern.isAssetLock(getOutput(i).getScriptPubKey())) + return i; + } + return -1; + } +} diff --git a/core/src/main/java/org/bitcoinj/evolution/CreditFundingTransaction.java b/core/src/main/java/org/bitcoinj/evolution/CreditFundingTransaction.java deleted file mode 100644 index 6d6343de45..0000000000 --- a/core/src/main/java/org/bitcoinj/evolution/CreditFundingTransaction.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2020 Dash Core Group - * - * 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.evolution; - -import org.bitcoinj.core.*; -import org.bitcoinj.crypto.DeterministicKey; -import org.bitcoinj.script.Script; -import org.bitcoinj.script.ScriptBuilder; -import org.bitcoinj.script.ScriptChunk; -import org.bitcoinj.script.ScriptPattern; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.bitcoinj.script.ScriptOpCodes.OP_RETURN; - -/** - * This class extends Transaction and is used to create a funding - * transaction for an identity. It also can store other information - * that is not stored in the blockchain transaction which includes - * the public or private key's associated with this transaction. - */ -public class CreditFundingTransaction extends Transaction { - - TransactionOutput lockedOutput; - TransactionOutPoint lockedOutpoint; - Coin fundingAmount; - Sha256Hash creditBurnIdentityIdentifier; - ECKey creditBurnPublicKey; - KeyId creditBurnPublicKeyId; - int usedDerivationPathIndex; - - - public CreditFundingTransaction(NetworkParameters params) { - super(params); - } - - /** - * Create a credit funding transaction from an existing transaction. - * This should only be called if {@link CreditFundingTransaction#isCreditFundingTransaction(Transaction)} - * returns true. - * @param tx this transaction should be a credit funding transaction - */ - public CreditFundingTransaction(Transaction tx) { - super(tx.getParams(), tx.bitcoinSerialize(), 0); - } - - /** - * Creates a credit funding transaction. - * @param params - * @param creditBurnKey The key from which the hash160 will be placed in the OP_RETURN output - * @param fundingAmount The amount of dash that will be locked in the OP_RETURN output - */ - public CreditFundingTransaction(NetworkParameters params, ECKey creditBurnKey, Coin fundingAmount) { - super(params); - this.fundingAmount = fundingAmount; - this.creditBurnPublicKey = creditBurnKey; - this.creditBurnPublicKeyId = KeyId.fromBytes(creditBurnPublicKey.getPubKeyHash()); - this.creditBurnIdentityIdentifier = Sha256Hash.ZERO_HASH; - if (creditBurnKey instanceof DeterministicKey) { - this.usedDerivationPathIndex = ((DeterministicKey)creditBurnKey).getChildNumber().num(); - } else this.usedDerivationPathIndex = -1; - ScriptBuilder builder = new ScriptBuilder().addChunk(new ScriptChunk(OP_RETURN, null)).data(creditBurnPublicKey.getPubKeyHash()); - lockedOutput = new TransactionOutput(params, null, fundingAmount, builder.build().getProgram()); - addOutput(lockedOutput); - } - - /** - * Creates a credit funding transaction by reading payload. - * Length of a transaction is fixed. - */ - - public CreditFundingTransaction(NetworkParameters params, byte [] payload) { - super(params, payload, 0); - } - - /** - * Deserialize and initialize some fields from the credit burn output - */ - @Override - protected void parse() throws ProtocolException { - super.parse(); - parseTransaction(); - } - - @Override - protected void unCache() { - super.unCache(); - lockedOutpoint = null; - lockedOutpoint = null; - creditBurnIdentityIdentifier = Sha256Hash.ZERO_HASH; - fundingAmount = Coin.ZERO; - creditBurnPublicKeyId = KeyId.KEYID_ZERO; - } - - /** - * Initializes lockedOutput, lockedOutpoint, fundingAmount and the hash160 - * credit burn key - */ - private void parseTransaction() { - getLockedOutput(); - getLockedOutpoint(); - fundingAmount = lockedOutput.getValue(); - getCreditBurnPublicKeyId(); - creditBurnIdentityIdentifier = getCreditBurnIdentityIdentifier(); - } - - /** - * Sets lockedOutput and returns output that has the OP_RETURN script - */ - public TransactionOutput getLockedOutput() { - if(lockedOutput != null) - return lockedOutput; - - for(TransactionOutput output : getOutputs()) { - Script script = output.getScriptPubKey(); - if(ScriptPattern.isCreditBurn(script)) { - lockedOutput = output; - return output; - } - } - return null; - } - - /** - * Sets lockedOutpoint and returns outpoint that has the OP_RETURN script - */ - public TransactionOutPoint getLockedOutpoint() { - if(lockedOutpoint != null) - return lockedOutpoint; - - for(int i = 0; i < getOutputs().size(); ++i) { - Script script = getOutput(i).getScriptPubKey(); - if(ScriptPattern.isCreditBurn(script)) { - // The lockedOutpoint must be in little endian to match Platform - // having a reversed txid will not allow it to be searched or matched. - lockedOutpoint = new TransactionOutPoint(params, i, Sha256Hash.wrap(getTxId().getReversedBytes())); - } - } - - return lockedOutpoint; - } - - public Coin getFundingAmount() { - return fundingAmount; - } - - /** - * Returns the credit burn identifier, which is the sha256(sha256(outpoint)) - */ - public Sha256Hash getCreditBurnIdentityIdentifier() { - if(creditBurnIdentityIdentifier == null || creditBurnIdentityIdentifier.isZero()) { - try { - ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(36); - getLockedOutpoint().bitcoinSerialize(bos); - creditBurnIdentityIdentifier = Sha256Hash.twiceOf(bos.toByteArray()); - } catch (IOException x) { - throw new RuntimeException(x); - } - } - return creditBurnIdentityIdentifier; - } - - public ECKey getCreditBurnPublicKey() { - return creditBurnPublicKey; - } - - public KeyId getCreditBurnPublicKeyId() { - if(creditBurnPublicKeyId == null || creditBurnPublicKeyId.equals(KeyId.KEYID_ZERO)) { - byte [] opReturnBytes = lockedOutput.getScriptPubKey().getChunks().get(1).data; - if(opReturnBytes.length == 20) - creditBurnPublicKeyId = KeyId.fromBytes(opReturnBytes); - } - return creditBurnPublicKeyId; - } - - public int getUsedDerivationPathIndex() { - return usedDerivationPathIndex; - } - - public void setCreditBurnPublicKeyAndIndex(ECKey creditBurnPublicKey, int usedDerivationPathIndex) { - this.creditBurnPublicKey = creditBurnPublicKey; - this.usedDerivationPathIndex = usedDerivationPathIndex; - } - - /** - * Determines if a transaction has one or more credit burn outputs - * and therefore is a is credit funding transaction - */ - public static boolean isCreditFundingTransaction(Transaction tx) { - for(TransactionOutput output : tx.getOutputs()) { - Script script = output.getScriptPubKey(); - if(ScriptPattern.isCreditBurn(script)) { - return true; - } - } - return false; - } - - /** - * Determines the first output that is a credit burn output - * or returns -1. - */ - public long getOutputIndex() { - int outputCount = getOutputs().size(); - for (int i = 0; i < outputCount; ++i) { - if (ScriptPattern.isCreditBurn(getOutput(i).getScriptPubKey())) - return i; - } - return -1; - } -} diff --git a/core/src/main/java/org/bitcoinj/evolution/listeners/CreditFundingTransactionEventListener.java b/core/src/main/java/org/bitcoinj/evolution/listeners/AssetLockTransactionEventListener.java similarity index 77% rename from core/src/main/java/org/bitcoinj/evolution/listeners/CreditFundingTransactionEventListener.java rename to core/src/main/java/org/bitcoinj/evolution/listeners/AssetLockTransactionEventListener.java index eb8d88661a..dd30dd8d5b 100644 --- a/core/src/main/java/org/bitcoinj/evolution/listeners/CreditFundingTransactionEventListener.java +++ b/core/src/main/java/org/bitcoinj/evolution/listeners/AssetLockTransactionEventListener.java @@ -18,19 +18,19 @@ import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.StoredBlock; -import org.bitcoinj.evolution.CreditFundingTransaction; +import org.bitcoinj.evolution.AssetLockTransaction; /** * */ -public interface CreditFundingTransactionEventListener { +public interface AssetLockTransactionEventListener { /** * * @param tx the credit funding transaction * @param block the block that the transaction was contained in, unless the tx is not confirmed * @param blockType main chain or side chain */ - void onTransactionReceived(CreditFundingTransaction tx, StoredBlock block, - BlockChain.NewBlockType blockType); + void onTransactionReceived(AssetLockTransaction tx, StoredBlock block, + BlockChain.NewBlockType blockType); } diff --git a/core/src/main/java/org/bitcoinj/script/Script.java b/core/src/main/java/org/bitcoinj/script/Script.java index 6bc59687e2..78e814cd17 100644 --- a/core/src/main/java/org/bitcoinj/script/Script.java +++ b/core/src/main/java/org/bitcoinj/script/Script.java @@ -61,7 +61,7 @@ public enum ScriptType { P2SH(3), // pay to script hash P2WPKH(4), // pay to witness pubkey hash P2WSH(5), // pay to witness script hash - CREDITBURN(6); // pay to credit system + ASSETLOCK(6); // pay to credit system public final int id; @@ -1630,8 +1630,8 @@ private byte[] getQuickProgram() { return ScriptType.P2PK; if (ScriptPattern.isP2SH(this)) return ScriptType.P2SH; - if(ScriptPattern.isCreditBurn(this)) - return ScriptType.CREDITBURN; + if(ScriptPattern.isAssetLock(this)) + return ScriptType.ASSETLOCK; return null; } diff --git a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java index e97b3f6858..3cff4a700f 100644 --- a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java +++ b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java @@ -563,10 +563,10 @@ public static Script createCLTVPaymentChannelInput(byte[] from, byte[] to) { return builder.build(); } - public static Script createCreditBurnOutput(ECKey creditBurnKey) { + public static Script createAssetLockOutput() { ScriptBuilder builder = new ScriptBuilder(); builder.addChunk(new ScriptChunk(OP_RETURN, null)); - builder.data(creditBurnKey.getPubKeyHash()); + builder.data(new byte[0]); return builder.build(); } } diff --git a/core/src/main/java/org/bitcoinj/script/ScriptPattern.java b/core/src/main/java/org/bitcoinj/script/ScriptPattern.java index 8421a649d9..049ec93433 100644 --- a/core/src/main/java/org/bitcoinj/script/ScriptPattern.java +++ b/core/src/main/java/org/bitcoinj/script/ScriptPattern.java @@ -18,11 +18,8 @@ package org.bitcoinj.script; import org.bitcoinj.core.Address; -import org.bitcoinj.core.Sha256Hash; -import org.bitcoinj.core.Utils; import java.math.BigInteger; -import java.util.Arrays; import java.util.List; import static org.bitcoinj.script.Script.decodeFromOpN; @@ -169,9 +166,9 @@ public static boolean isSentToMultisig(Script script) { /** * Returns whether this script matches the format used for credit burn outputs: - * {@code OP_RETURN [20] [creditBurnKeyId]} + * {@code OP_RETURN 0[]} */ - public static boolean isCreditBurn(Script script) { + public static boolean isAssetLock(Script script) { List chunks = script.chunks; if (chunks.size() != 2) return false; @@ -180,11 +177,7 @@ public static boolean isCreditBurn(Script script) { return false; ScriptChunk chunk1 = chunks.get(1); byte[] chunk1data = chunk1.data; - if (chunk1data == null) - return false; - if (chunk1data.length != 20) - return false; - return true; + return chunk1data != null && chunk1data.length == 0; } /** @@ -240,8 +233,4 @@ public static boolean isOpReturn(Script script) { List chunks = script.chunks; return chunks.size() > 0 && chunks.get(0).equalsOpCode(ScriptOpCodes.OP_RETURN); } - - public static byte[] extractCreditBurnKeyId(Script script) { - return script.chunks.get(1).data; - } } \ No newline at end of file diff --git a/core/src/main/java/org/bitcoinj/wallet/SendRequest.java b/core/src/main/java/org/bitcoinj/wallet/SendRequest.java index 723c991a93..f7857c5b83 100644 --- a/core/src/main/java/org/bitcoinj/wallet/SendRequest.java +++ b/core/src/main/java/org/bitcoinj/wallet/SendRequest.java @@ -33,7 +33,7 @@ import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; -import org.bitcoinj.evolution.CreditFundingTransaction; +import org.bitcoinj.evolution.AssetLockTransaction; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.utils.ExchangeRate; @@ -239,9 +239,19 @@ public static SendRequest emptyWallet(Address destination) { /** *

Creates a new credit funding transaction for a public key with a funding amount.

*/ + @Deprecated public static SendRequest creditFundingTransaction(NetworkParameters params, ECKey publicKey, Coin credits) { SendRequest req = new SendRequest(); - req.tx = new CreditFundingTransaction(params, publicKey, credits); + req.tx = new AssetLockTransaction(params, publicKey, credits); + return req; + } + + /** + *

Creates a new asset lock transaction for a public key with a funding amount.

+ */ + public static SendRequest assetLock(NetworkParameters params, ECKey publicKey, Coin credits) { + SendRequest req = new SendRequest(); + req.tx = new AssetLockTransaction(params, publicKey, credits); return req; } diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index 38b5e44d94..163d9dbb83 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -56,6 +56,8 @@ import org.bitcoinj.core.Peer; import org.bitcoinj.core.PeerFilterProvider; import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.evolution.AssetLockPayload; +import org.bitcoinj.evolution.SpecialTxPayload; import org.bitcoinj.script.ScriptException; import org.bitcoinj.core.TransactionConfidence.*; import org.bitcoinj.crypto.*; @@ -1352,9 +1354,9 @@ private void markKeysAsUsed(Transaction tx) { byte[] pubkey = ScriptPattern.extractKeyFromP2PK(script); keyChainGroup.markPubKeyAsUsed(pubkey); if (receivingFromFriendsGroup != null) - receivingFromFriendsGroup.markPubKeyAsUsed(pubkey); + receivingFromFriendsGroup.markPubKeyAsUsed(pubkey); if (sendingToFriendsGroup != null) - sendingToFriendsGroup.markPubKeyAsUsed(pubkey); + sendingToFriendsGroup.markPubKeyAsUsed(pubkey); for (KeyChainGroupExtension extension : keyChainExtensions.values()) { extension.markPubKeyAsUsed(pubkey); } @@ -1363,9 +1365,9 @@ private void markKeysAsUsed(Transaction tx) { byte[] pubkeyHash = ScriptPattern.extractHashFromP2PKH(script); keyChainGroup.markPubKeyHashAsUsed(pubkeyHash); if (receivingFromFriendsGroup != null) - receivingFromFriendsGroup.markPubKeyHashAsUsed(pubkeyHash); + receivingFromFriendsGroup.markPubKeyHashAsUsed(pubkeyHash); if (sendingToFriendsGroup != null) - sendingToFriendsGroup.markPubKeyHashAsUsed(pubkeyHash); + sendingToFriendsGroup.markPubKeyHashAsUsed(pubkeyHash); for (KeyChainGroupExtension extension : keyChainExtensions.values()) { extension.markPubKeyHashAsUsed(pubkeyHash); } @@ -1374,23 +1376,30 @@ private void markKeysAsUsed(Transaction tx) { ScriptPattern.extractHashFromP2SH(script)); keyChainGroup.markP2SHAddressAsUsed(a); if (receivingFromFriendsGroup != null) - receivingFromFriendsGroup.markP2SHAddressAsUsed(a); + receivingFromFriendsGroup.markP2SHAddressAsUsed(a); if (sendingToFriendsGroup != null) - sendingToFriendsGroup.markP2SHAddressAsUsed(a); + sendingToFriendsGroup.markP2SHAddressAsUsed(a); for (KeyChainGroupExtension extension : keyChainExtensions.values()) { extension.markP2SHAddressAsUsed(a); } - } else if (ScriptPattern.isCreditBurn(script)) { - byte [] h = ScriptPattern.extractCreditBurnKeyId(script); - for (KeyChainGroupExtension extension : keyChainExtensions.values()) { - extension.markPubKeyHashAsUsed(h); - } } } catch (ScriptException e) { // Just means we didn't understand the output of this transaction: ignore it. log.warn("Could not parse tx output script: {}", e.toString()); } } + + // handle special transactions + SpecialTxPayload payload = tx.getExtraPayloadObject(); + if (payload instanceof AssetLockPayload) { + AssetLockPayload assetLockPayload = (AssetLockPayload) payload; + for (TransactionOutput output : assetLockPayload.getCreditOutputs()) { + byte [] h = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey()); + for (KeyChainGroupExtension extension : keyChainExtensions.values()) { + extension.markPubKeyHashAsUsed(h); + } + } + } } finally { keyChainGroupLock.unlock(); } @@ -5429,6 +5438,8 @@ private FeeCalculation calculateFee(SendRequest req, Coin value, List selectCoinsGroupedByAddresses(boolean skipDenomina } + /** + * Count the number of unspent outputs that have a certain value + */ public int countInputsWithAmount(Coin inputValue) { int count = 0; - for (Transaction tx : getTransactionPool(WalletTransaction.Pool.UNSPENT).values()) { - for (TransactionOutput output : tx.getOutputs()) { - if (output.getValue().equals(inputValue) && output.isMine(this)) + for (TransactionOutput output : myUnspents) { + TransactionConfidence confidence = output.getParentTransaction().getConfidence(); + // confirmations must be 0 or higher, not conflicted or dead + if (confidence != null && (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING || confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING)) { + // inputValue must match, the TX is mine and is not spent + if (output.getValue().equals(inputValue) && output.getSpentBy() == null) { count++; + } } } return count; } + /** locks an unspent outpoint so that it cannot be spent */ public boolean lockCoin(TransactionOutPoint outPoint) { - lockedCoinsSet.add(outPoint); + boolean added = lockedCoinsSet.add(outPoint); clearAnonymizableCaches(); - return false; + return added; } + /** unlocks an outpoint so that it cannot be spent */ public void unlockCoin(TransactionOutPoint outPoint) { lockedCoinsSet.remove(outPoint); clearAnonymizableCaches(); @@ -973,4 +986,43 @@ List getCoinJoinOutputs() { } return result; } + + public String getTransactionReport() { + MonetaryFormat format = MonetaryFormat.BTC.noCode(); + StringBuilder s = new StringBuilder("Transaction History Report"); + s.append("\n-----------------------------------------------\n"); + + ArrayList sortedTxes = Lists.newArrayList(); + getWalletTransactions().forEach(tx -> sortedTxes.add(tx.getTransaction())); + sortedTxes.sort(Transaction.SORT_TX_BY_UPDATE_TIME); + + sortedTxes.forEach(tx -> { + final Coin value = tx.getValue(this); + s.append(Utils.dateTimeFormat(tx.getUpdateTime())).append(" "); + s.append(String.format("%14s", format.format(value))).append(" "); + final CoinJoinTransactionType type = CoinJoinTransactionType.fromTx(tx, this); + + // TX type + String txType; + if (type != CoinJoinTransactionType.None) { + txType = type.toString(); + } else { + if (value.isGreaterThan(Coin.ZERO)) { + txType = "Received"; + } else { + txType = "Sent"; + } + } + s.append(String.format("%-20s", txType)); + s.append(" "); + s.append(tx.getTxId()); + s.append("\n"); + }); + return s.toString(); + } + + @Override + public String toString(boolean includeLookahead, boolean includePrivateKeys, @Nullable KeyParameter aesKey, boolean includeTransactions, boolean includeExtensions, @Nullable AbstractBlockChain chain) { + return super.toString(includeLookahead, includePrivateKeys, aesKey, includeTransactions, includeExtensions, chain) + getTransactionReport(); + } } \ No newline at end of file diff --git a/core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java b/core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java index 0525fbb0e0..44b4bbfe48 100644 --- a/core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java +++ b/core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java @@ -42,12 +42,12 @@ import org.bitcoinj.crypto.factory.ECKeyFactory; import org.bitcoinj.crypto.factory.Ed25519KeyFactory; import org.bitcoinj.crypto.factory.KeyFactory; -import org.bitcoinj.evolution.CreditFundingTransaction; +import org.bitcoinj.evolution.AssetLockTransaction; import org.bitcoinj.evolution.ProviderRegisterTx; import org.bitcoinj.evolution.ProviderUpdateRegistarTx; import org.bitcoinj.evolution.ProviderUpdateRevocationTx; import org.bitcoinj.evolution.ProviderUpdateServiceTx; -import org.bitcoinj.evolution.listeners.CreditFundingTransactionEventListener; +import org.bitcoinj.evolution.listeners.AssetLockTransactionEventListener; import org.bitcoinj.script.Script; import org.bitcoinj.utils.ListenerRegistration; import org.bitcoinj.utils.Threading; @@ -96,7 +96,7 @@ public class AuthenticationGroupExtension extends AbstractKeyChainGroupExtension private AuthenticationKeyChainGroup keyChainGroup; private final HashMap keyUsage = Maps.newHashMap(); - private final CopyOnWriteArrayList> creditFundingListeners + private final CopyOnWriteArrayList> creditFundingListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList> usageListeners = new CopyOnWriteArrayList<>(); @@ -376,8 +376,8 @@ public void processTransaction(Transaction tx, StoredBlock block, BlockChain.New break; } } else { - if(CreditFundingTransaction.isCreditFundingTransaction(tx)) { - CreditFundingTransaction cftx = getCreditFundingTransaction(tx); + if(AssetLockTransaction.isAssetLockTransaction(tx)) { + AssetLockTransaction cftx = getAssetLockTransaction(tx); if (cftx != null) queueOnCreditFundingEvent(cftx, block, blockType); } @@ -650,19 +650,19 @@ private static ImmutableList getDefaultPath(NetworkParameters param } } - HashMap mapCreditFundingTxs = new HashMap<>(); + HashMap mapCreditFundingTxs = new HashMap<>(); /** * @return list of credit funding transactions found in the wallet. */ - public List getCreditFundingTransactions() { + public List getAssetLockTransactions() { mapCreditFundingTxs.clear(); - ArrayList txs = new ArrayList<>(1); + ArrayList txs = new ArrayList<>(1); for(WalletTransaction wtx : wallet.getWalletTransactions()) { Transaction tx = wtx.getTransaction(); - if(CreditFundingTransaction.isCreditFundingTransaction(tx)) { - CreditFundingTransaction cftx = getCreditFundingTransaction(tx); + if(AssetLockTransaction.isAssetLockTransaction(tx)) { + AssetLockTransaction cftx = getAssetLockTransaction(tx); if (cftx != null) { txs.add(cftx); mapCreditFundingTxs.put(cftx.getTxId(), cftx); @@ -692,24 +692,24 @@ public AuthenticationKeyChain getInvitationFundingKeyChain() { return keyChainGroup.getKeyChain(AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING); } - public List getIdentityFundingTransactions() { - return getFundingTransactions(getIdentityFundingKeyChain()); + public List getIdentityFundingTransactions() { + return getAssetLockTransactions(getIdentityFundingKeyChain()); } - public List getTopupFundingTransactions() { - return getFundingTransactions(getIdentityTopupKeyChain()); + public List getTopupFundingTransactions() { + return getAssetLockTransactions(getIdentityTopupKeyChain()); } - public List getInvitationFundingTransactions() { - return getFundingTransactions(getInvitationFundingKeyChain()); + public List getInvitationFundingTransactions() { + return getAssetLockTransactions(getInvitationFundingKeyChain()); } - private List getFundingTransactions(AuthenticationKeyChain chain) { - ArrayList txs = new ArrayList<>(1); - List allTxs = getCreditFundingTransactions(); + private List getAssetLockTransactions(AuthenticationKeyChain chain) { + ArrayList txs = new ArrayList<>(1); + List allTxs = getAssetLockTransactions(); - for (CreditFundingTransaction cftx : allTxs) { - if(chain.findKeyFromPubHash(cftx.getCreditBurnPublicKeyId().getBytes()) != null) { + for (AssetLockTransaction cftx : allTxs) { + if(chain.findKeyFromPubHash(cftx.getAssetLockPublicKeyId().getBytes()) != null) { txs.add(cftx); } } @@ -719,34 +719,34 @@ private List getFundingTransactions(AuthenticationKeyC /** * Get a CreditFundingTransaction object for a specific transaction */ - public CreditFundingTransaction getCreditFundingTransaction(Transaction tx) { + public AssetLockTransaction getAssetLockTransaction(Transaction tx) { if (getIdentityFundingKeyChain() == null) return null; if (mapCreditFundingTxs.containsKey(tx.getTxId())) return mapCreditFundingTxs.get(tx.getTxId()); - CreditFundingTransaction cftx = new CreditFundingTransaction(tx); + AssetLockTransaction cftx = new AssetLockTransaction(tx); // set some internal data for the transaction - DeterministicKey publicKey = (DeterministicKey) getIdentityFundingKeyChain().getKeyByPubKeyHash(cftx.getCreditBurnPublicKeyId().getBytes()); + DeterministicKey publicKey = (DeterministicKey) getIdentityFundingKeyChain().getKeyByPubKeyHash(cftx.getAssetLockPublicKeyId().getBytes()); if (publicKey == null) - publicKey = (DeterministicKey) getIdentityTopupKeyChain().getKeyByPubKeyHash(cftx.getCreditBurnPublicKeyId().getBytes()); + publicKey = (DeterministicKey) getIdentityTopupKeyChain().getKeyByPubKeyHash(cftx.getAssetLockPublicKeyId().getBytes()); if (publicKey == null) - publicKey = (DeterministicKey) getInvitationFundingKeyChain().getKeyByPubKeyHash(cftx.getCreditBurnPublicKeyId().getBytes()); + publicKey = (DeterministicKey) getInvitationFundingKeyChain().getKeyByPubKeyHash(cftx.getAssetLockPublicKeyId().getBytes()); if(publicKey != null) - cftx.setCreditBurnPublicKeyAndIndex(publicKey, publicKey.getChildNumber().num()); - else log.error("Cannot find " + KeyId.fromBytes(cftx.getCreditBurnPublicKeyId().getBytes()) + " in the wallet"); + cftx.addAssetLockPublicKey(publicKey); + else log.error("Cannot find " + KeyId.fromBytes(cftx.getAssetLockPublicKeyId().getBytes()) + " in the wallet"); mapCreditFundingTxs.put(cftx.getTxId(), cftx); return cftx; } - protected void queueOnCreditFundingEvent(final CreditFundingTransaction tx, StoredBlock block, + protected void queueOnCreditFundingEvent(final AssetLockTransaction tx, StoredBlock block, BlockChain.NewBlockType blockType) { - for (final ListenerRegistration registration : creditFundingListeners) { + for (final ListenerRegistration registration : creditFundingListeners) { registration.executor.execute(new Runnable() { @Override public void run() { @@ -760,7 +760,7 @@ public void run() { * Adds an credit funding event listener object. Methods on this object are called when * a credit funding transaction is created or received. */ - public void addCreditFundingEventListener(CreditFundingTransactionEventListener listener) { + public void addCreditFundingEventListener(AssetLockTransactionEventListener listener) { addCreditFundingEventListener(Threading.USER_THREAD, listener); } @@ -768,7 +768,7 @@ public void addCreditFundingEventListener(CreditFundingTransactionEventListener * Adds an credit funding event listener object. Methods on this object are called when * a credit funding transaction is created or received. */ - public void addCreditFundingEventListener(Executor executor, CreditFundingTransactionEventListener listener) { + public void addCreditFundingEventListener(Executor executor, AssetLockTransactionEventListener listener) { // This is thread safe, so we don't need to take the lock. creditFundingListeners.add(new ListenerRegistration<>(listener, executor)); } @@ -777,7 +777,7 @@ public void addCreditFundingEventListener(Executor executor, CreditFundingTransa * Removes the given event listener object. Returns true if the listener was removed, false if that listener * was never added. */ - public boolean removeCreditFundingEventListener(CreditFundingTransactionEventListener listener) { + public boolean removeCreditFundingEventListener(AssetLockTransactionEventListener listener) { return ListenerRegistration.removeFromList(listener, creditFundingListeners); } diff --git a/core/src/test/java/org/bitcoinj/evolution/AssetLockTest.java b/core/src/test/java/org/bitcoinj/evolution/AssetLockTest.java index 172be5403b..39f51e09f4 100644 --- a/core/src/test/java/org/bitcoinj/evolution/AssetLockTest.java +++ b/core/src/test/java/org/bitcoinj/evolution/AssetLockTest.java @@ -1,6 +1,9 @@ package org.bitcoinj.evolution; import com.google.common.collect.Lists; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Context; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; @@ -9,20 +12,25 @@ import org.bitcoinj.params.UnitTestParams; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptPattern; import org.bitcoinj.wallet.DefaultRiskAnalysis; +import org.bitcoinj.wallet.SendRequest; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import static org.bitcoinj.core.Coin.CENT; +import static org.bitcoinj.core.Coin.COIN; import static org.bitcoinj.core.Utils.HEX; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; public class AssetLockTest { UnitTestParams PARAMS = UnitTestParams.get(); + Context context = new Context(PARAMS); ArrayList dummyTransactions; ArrayList dummyOutputs; @@ -125,6 +133,22 @@ public void assetLockTransaction() { assertEquals(1, assetLockPayload.getVersion()); assertEquals(Transaction.Type.TRANSACTION_ASSET_LOCK, assetLockPayload.getType()); assertEquals(2, assetLockPayload.getCreditOutputs().size()); + assertEquals(Coin.COIN, assetLockPayload.getCreditOutputs().get(0).getValue()); + assertEquals(ScriptBuilder.createOutputScript(Address.fromBase58(PARAMS, "yRM7hdX9SoP5giL37oX11fdWofFf2t9kUv")), assetLockPayload.getCreditOutputs().get(0).getScriptPubKey()); + assertTrue(tx.getOutputs().stream().anyMatch(output -> ScriptPattern.isAssetLock(output.getScriptPubKey()))); + } + + @Test + public void createAssetLockTransaction() { + ECKey privateKey = new ECKey(); + Coin credits = COIN; + SendRequest request = SendRequest.assetLock(PARAMS, privateKey, credits); + AssetLockPayload payload = (AssetLockPayload) request.tx.getExtraPayloadObject(); + assertEquals(AssetLockPayload.CURRENT_VERSION, payload.getVersion()); + assertEquals(1, payload.getCreditOutputs().size()); + assertEquals(COIN, payload.getCreditOutputs().get(0).getValue()); + assertArrayEquals(privateKey.getPubKeyHash(), ScriptPattern.extractHashFromP2PKH(payload.getCreditOutputs().get(0).getScriptPubKey())); + assertTrue(request.tx.getOutputs().stream().anyMatch(output -> ScriptPattern.isAssetLock(output.getScriptPubKey()))); } @Test diff --git a/core/src/test/java/org/bitcoinj/evolution/AssetLockTransactionTest.java b/core/src/test/java/org/bitcoinj/evolution/AssetLockTransactionTest.java new file mode 100644 index 0000000000..8b660075c0 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/evolution/AssetLockTransactionTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2020 Dash Core Group + * + * 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.evolution; + +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.IDeterministicKey; +import org.bitcoinj.params.UnitTestParams; +import org.bitcoinj.script.Script; +import org.bitcoinj.wallet.AuthenticationKeyChain; +import org.bitcoinj.wallet.AuthenticationKeyChainGroup; +import org.bitcoinj.wallet.DerivationPathFactory; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.UnreadableWalletException; +import org.bitcoinj.wallet.Wallet; +import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension; +import org.bouncycastle.util.encoders.Base64; +import org.junit.Before; +import org.junit.Test; + +import java.util.EnumSet; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + + +public class AssetLockTransactionTest { + + Context context; + UnitTestParams PARAMS; + + byte[] transactionData = Utils.HEX.decode("030008000142b021d1ee5752ec0bdd0f231fdafa7d59494985c32be5fc66cba890c5da3a8c010000006b483045022100e193274c1ea61cac9dd6ce9b93c29aa2037fdec8ae85a0355d419879bae3c2fa02201e8351cd09e9a4f5f416b362714cfb636513da6cd29fc01d183adb6f85ce22f00121020efe2eed0792b52f2f08c3fe558350e56f87ebd6a6e1d18e3671030bd59797a1ffffffff02c0e1e40000000000026a006fc92302000000001976a914fb481507faa32b529ff526871eb4a0feddd2cbdc88ac00000000240101c0e1e400000000001976a91495b02bf4a33ff0a95b18e8c54ac0a4f131d1ba7488ac"); + + + @Before + public void startup() { + PARAMS = UnitTestParams.get(); + context = new Context(PARAMS); + } + + @Test + public void testIdentityAssetLockTransactionUniqueId() { + AssetLockTransaction fundingTransaction = new AssetLockTransaction(PARAMS, transactionData); + + String lockedOutpoint = fundingTransaction.getLockedOutpoint().toStringBase64(); + assertEquals("Locked outpoint is incorrect", "nQxmWXkiwGZ6VzBk72ukhBKuTZH/AZgfx8pAvPbVbPIAAAAA", lockedOutpoint); + String identityIdentifier = fundingTransaction.getIdentityId().toStringBase58(); + assertEquals("Identity Identifier is incorrect", "451RMdWKovJWbfx5Q84oihL3n2J1N4p5E8SxCarT1ijF", identityIdentifier); + + ECKey privateKey = ECKey.fromPrivate(Utils.HEX.decode("8cfb151eceb7540e42da177cfbe3a1580e97edf96a699e40d100cea669abb002"),true); + assertArrayEquals("The private key doesn't match the funding transaction", privateKey.getPubKeyHash(), fundingTransaction.getAssetLockPublicKeyId().getBytes()); + + } + + @Test + public void constructorTest() { + AssetLockTransaction cftx = new AssetLockTransaction(PARAMS, transactionData); + Transaction tx = new Transaction(PARAMS, transactionData); + assertEquals(true, AssetLockTransaction.isAssetLockTransaction(tx)); + AssetLockTransaction cftxCopy = new AssetLockTransaction(tx); + + assertEquals(cftx.getFundingAmount(), cftxCopy.getFundingAmount()); + assertEquals(cftx.getIdentityId(), cftxCopy.getIdentityId()); + + ECKey publicKey = ECKey.fromPublicOnly(Base64.decode("AsPvyyh6pkxss/Fespa7HCJIY8IA6ElAf6VKuqVcnPze")); + cftx = new AssetLockTransaction(PARAMS, publicKey, Coin.valueOf(10000)); + cftx.toString(); //make sure it doesn't crash + } + + + /* + from dashj: + ----------- + AssetLockTransaction{9d0c66597922c0667a573064ef6ba48412ae4d91ff01981fc7ca40bcf6d56cf2 + version: 3 + type: TRANSACTION_ASSET_LOCK(8) + purpose: UNKNOWN + in PUSHDATA(72)[3045022100e193274c1ea61cac9dd6ce9b93c29aa2037fdec8ae85a0355d419879bae3c2fa02201e8351cd09e9a4f5f416b362714cfb636513da6cd29fc01d183adb6f85ce22f001] PUSHDATA(33)[020efe2eed0792b52f2f08c3fe558350e56f87ebd6a6e1d18e3671030bd59797a1] + unconnected outpoint:8c3adac590a8cb66fce52bc3854949597dfada1f230fdd0bec5257eed121b042:1 + out RETURN 0[] 0.15 DASH + ASSETLOCK + out DUP HASH160 PUSHDATA(20)[fb481507faa32b529ff526871eb4a0feddd2cbdc] EQUALVERIFY CHECKSIG 0.35899759 DASH + P2PKH addr:yjE6oi2vV48uTiqR82RnaH5NDvFG13L9vh + payload: AssetLockPayload + out DUP HASH160 PUSHDATA(20)[95b02bf4a33ff0a95b18e8c54ac0a4f131d1ba74] EQUALVERIFY CHECKSIG 0.15 DASH + P2PKH addr:yZxvaQkCdZKZtLwtETcHhQ7JXaaEJShfho + } + + + From the Testnet blockchain: + --------------------------- + getrawtransaction 9d0c66597922c0667a573064ef6ba48412ae4d91ff01981fc7ca40bcf6d56cf2 true + { + "txid": "9d0c66597922c0667a573064ef6ba48412ae4d91ff01981fc7ca40bcf6d56cf2", + "version": 3, + "type": 8, + "size": 240, + "locktime": 0, + "vin": [ + { + "txid": "8c3adac590a8cb66fce52bc3854949597dfada1f230fdd0bec5257eed121b042", + "vout": 1, + "scriptSig": { + "asm": "3045022100e193274c1ea61cac9dd6ce9b93c29aa2037fdec8ae85a0355d419879bae3c2fa02201e8351cd09e9a4f5f416b362714cfb636513da6cd29fc01d183adb6f85ce22f0[ALL] 020efe2eed0792b52f2f08c3fe558350e56f87ebd6a6e1d18e3671030bd59797a1", + "hex": "483045022100e193274c1ea61cac9dd6ce9b93c29aa2037fdec8ae85a0355d419879bae3c2fa02201e8351cd09e9a4f5f416b362714cfb636513da6cd29fc01d183adb6f85ce22f00121020efe2eed0792b52f2f08c3fe558350e56f87ebd6a6e1d18e3671030bd59797a1" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.15000000, + "valueSat": 15000000, + "n": 0, + "scriptPubKey": { + "asm": "OP_RETURN 0", + "hex": "6a00", + "type": "nulldata" + } + }, + { + "value": 0.35899759, + "valueSat": 35899759, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 fb481507faa32b529ff526871eb4a0feddd2cbdc OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914fb481507faa32b529ff526871eb4a0feddd2cbdc88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "yjE6oi2vV48uTiqR82RnaH5NDvFG13L9vh" + ] + } + } + ], + "extraPayloadSize": 36, + "extraPayload": "0101c0e1e400000000001976a91495b02bf4a33ff0a95b18e8c54ac0a4f131d1ba7488ac", + "assetLockTx": { + "version": 1, + "creditOutputs": [ + "CTxOut(nValue=0.15000000, scriptPubKey=76a91495b02bf4a33ff0a95b18e8c5)" + ] + }, + "hex": "030008000142b021d1ee5752ec0bdd0f231fdafa7d59494985c32be5fc66cba890c5da3a8c010000006b483045022100e193274c1ea61cac9dd6ce9b93c29aa2037fdec8ae85a0355d419879bae3c2fa02201e8351cd09e9a4f5f416b362714cfb636513da6cd29fc01d183adb6f85ce22f00121020efe2eed0792b52f2f08c3fe558350e56f87ebd6a6e1d18e3671030bd59797a1ffffffff02c0e1e40000000000026a006fc92302000000001976a914fb481507faa32b529ff526871eb4a0feddd2cbdc88ac00000000240101c0e1e400000000001976a91495b02bf4a33ff0a95b18e8c54ac0a4f131d1ba7488ac", + "instantlock": true, + "instantlock_internal": true, + "chainlock": false + } + + */ + + @Test + public void creditFundingFromWalletTest() throws UnreadableWalletException { + // recovery phrase from a wallet that was used to generate a credit funding transaction + // this is generated using the CreateWallet example in android-dashpay + String mnemonic = "language turn degree dignity census faculty usual special claim sausage staff faint"; + DerivationPathFactory factory = new DerivationPathFactory(PARAMS); + + // Create the keychain for blockchain identity funding + AuthenticationKeyChain blockchainIdentityFundingChain = AuthenticationKeyChain.authenticationBuilder() + .accountPath(factory.blockchainIdentityRegistrationFundingDerivationPath()) + .seed(new DeterministicSeed(mnemonic, null, "", Utils.currentTimeSeconds())) + .type(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING) + .build(); + + // create the authentication key chain group and add the blockchain identity funding key chain + AuthenticationKeyChainGroup group = AuthenticationKeyChainGroup.authenticationBuilder(PARAMS) + .addChain(blockchainIdentityFundingChain) + .build(); + + Wallet wallet = Wallet.createDeterministic(context, Script.ScriptType.P2PKH); + AuthenticationGroupExtension authenticationGroupExtension = new AuthenticationGroupExtension(wallet); + wallet.addExtension(authenticationGroupExtension); + authenticationGroupExtension.addKeyChains(context.getParams(), new DeterministicSeed(mnemonic, null, "", Utils.currentTimeSeconds()), EnumSet.of(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING)); + + + // Get the first key from the blockchain identity funding keychain + IDeterministicKey firstKey = authenticationGroupExtension.getKeyChain(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING).getKey(1); + + /* tx data for 9d0c66597922c0667a573064ef6ba48412ae4d91ff01981fc7ca40bcf6d56cf2 */ + AssetLockTransaction cftx = new AssetLockTransaction(PARAMS, transactionData); + + // compare the credit burn public key id to the public key hash of the first key + assertArrayEquals(cftx.getAssetLockPublicKeyId().getBytes(), firstKey.getPubKeyHash()); + } +} diff --git a/core/src/test/java/org/bitcoinj/evolution/CreditFundingTransactionTest.java b/core/src/test/java/org/bitcoinj/evolution/CreditFundingTransactionTest.java deleted file mode 100644 index 257972b6fe..0000000000 --- a/core/src/test/java/org/bitcoinj/evolution/CreditFundingTransactionTest.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2020 Dash Core Group - * - * 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.evolution; - -import org.bitcoinj.core.*; -import org.bitcoinj.crypto.IDeterministicKey; -import org.bitcoinj.params.UnitTestParams; -import org.bitcoinj.wallet.AuthenticationKeyChain; -import org.bitcoinj.wallet.AuthenticationKeyChainGroup; -import org.bitcoinj.wallet.DerivationPathFactory; -import org.bitcoinj.wallet.DeterministicSeed; -import org.bitcoinj.wallet.UnreadableWalletException; -import org.bouncycastle.util.encoders.Base64; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - - -public class CreditFundingTransactionTest { - - Context context; - UnitTestParams PARAMS; - - byte[] transactionData = Utils.HEX.decode("0300000002b74030bbda6edd804d4bfb2bdbbb7c207a122f3af2f6283de17074a42c6a5417020000006b483045022100815b175ab1a8fde7d651d78541ba73d2e9b297e6190f5244e1957004aa89d3c902207e1b164499569c1f282fe5533154495186484f7db22dc3dc1ccbdc9b47d997250121027f69794d6c4c942392b1416566aef9eaade43fbf07b63323c721b4518127baadffffffffb74030bbda6edd804d4bfb2bdbbb7c207a122f3af2f6283de17074a42c6a5417010000006b483045022100a7c94fe1bb6ffb66d2bb90fd8786f5bd7a0177b0f3af20342523e64291f51b3e02201f0308f1034c0f6024e368ca18949be42a896dda434520fa95b5651dc5ad3072012102009e3f2eb633ee12c0143f009bf773155a6c1d0f14271d30809b1dc06766aff0ffffffff031027000000000000166a1414ec6c36e6c39a9181f3a261a08a5171425ac5e210270000000000001976a91414ec6c36e6c39a9181f3a261a08a5171425ac5e288acc443953b000000001976a9140d1775b9ed85abeb19fd4a7d8cc88b08a29fe6de88ac00000000"); - - - @Before - public void startup() { - PARAMS = UnitTestParams.get(); - context = new Context(PARAMS); - } - - /** - * taken from dashsync-iOS - */ - @Test - public void testBlockchainIdentityFundingTransactionUniqueId() { - CreditFundingTransaction fundingTransaction = new CreditFundingTransaction(PARAMS, transactionData); - - String lockedOutpoint = fundingTransaction.lockedOutpoint.toStringBase64(); - assertEquals("Locked outpoint is incorrect", "pRtcx0tE0ydkGODlBEfWNIivD2w6whvSkvYunB5+hCUAAAAA", lockedOutpoint); - String identityIdentifier = fundingTransaction.creditBurnIdentityIdentifier.toStringBase58(); - assertEquals("Identity Identifier is incorrect", "Cka1ELdpfrZhFFvKRurvPtTHurDXXnnezafNPJkxCYjc", identityIdentifier); - - ECKey publicKey = ECKey.fromPublicOnly(Base64.decode("AsPvyyh6pkxss/Fespa7HCJIY8IA6ElAf6VKuqVcnPze")); - ECKey privateKey = ECKey.fromPrivate(Utils.HEX.decode("fdbca0cd2be4375f04fcaee5a61c5d170a2a46b1c0c7531f58c430734a668f32"),true); - assertArrayEquals("The private key doesn't match the funding transaction", privateKey.getPubKeyHash(), fundingTransaction.creditBurnPublicKeyId.getBytes()); - - } - - @Test - public void constructorTest() { - CreditFundingTransaction cftx = new CreditFundingTransaction(PARAMS, transactionData); - Transaction tx = new Transaction(PARAMS, transactionData); - assertEquals(true, CreditFundingTransaction.isCreditFundingTransaction(tx)); - CreditFundingTransaction cftxCopy = new CreditFundingTransaction(tx); - - assertEquals(cftx.getFundingAmount(), cftxCopy.getFundingAmount()); - assertEquals(cftx.getCreditBurnIdentityIdentifier(), cftxCopy.getCreditBurnIdentityIdentifier()); - - ECKey publicKey = ECKey.fromPublicOnly(Base64.decode("AsPvyyh6pkxss/Fespa7HCJIY8IA6ElAf6VKuqVcnPze")); - cftx = new CreditFundingTransaction(PARAMS, publicKey, Coin.valueOf(10000)); - cftx.toString(); //make sure it doesn't crash - } - - - /* - from dashj: - ----------- - -0.00040224 DASH total value (sends 0.01 DASH and receives 0.00959776 DASH) - a8abd4337d4906041324f1c9f713342112c0d24655fd295a57fe3563ac969896 - updated: 2020-02-26T02:13:00Z - type TRANSACTION_NORMAL(0) - purpose: USER_PAYMENT - in PUSHDATA(71)[304402207db8b157eeb8988556a03e817fd44f24e56c2c9341c5a0e512a89607481b8cb002204e7c3831e1b522e0ef740cf31926fa0e4e6cdd6496cde5005ff29f6b4499205801] PUSHDATA(33)[023bd027cf07a4eab516e22db16c7d77bb615bcd25a57bee6ee69dfaef23a61dac] 0.01 DASH - P2PKH addr:yWJ58NBiirQjuWQPe9aCK1XwMYbya9kRCm outpoint:2a93fefe869367a479496484421934ed81d0e9544b42a12d579fcab9f672e7fd:0 - out RETURN PUSHDATA(20)[d0949bd75de50fbba5081d932fea0a0ee7407fa5] 0.0004 DASH - CREDITBURN addr:yfLKRcydTC8naznVHGVwAExyewAsayLcst - out DUP HASH160 PUSHDATA(20)[29f4856f82e55313c8e7bf791ac868924ec9c59b] EQUALVERIFY CHECKSIG 0.00959776 DASH - P2PKH addr:yQ9HUhd7DvVwUAXqRKWsJmgnVsVmjAq8V7 - fee 0.00001009 DASH/kB, 0.00000224 DASH for 222 bytes - - - From the Evonet blockchain: - --------------------------- - getrawtransaction a8abd4337d4906041324f1c9f713342112c0d24655fd295a57fe3563ac969896 true - { - "hex": "0100000001fde772f6b9ca9f572da1424b54e9d081ed34194284644979a4679386fefe932a000000006a47304402207db8b157eeb8988556a03e817fd44f24e56c2c9341c5a0e512a89607481b8cb002204e7c3831e1b522e0ef740cf31926fa0e4e6cdd6496cde5005ff29f6b449920580121023bd027cf07a4eab516e22db16c7d77bb615bcd25a57bee6ee69dfaef23a61dacffffffff02409c000000000000166a14d0949bd75de50fbba5081d932fea0a0ee7407fa520a50e00000000001976a91429f4856f82e55313c8e7bf791ac868924ec9c59b88ac00000000", - "txid": "a8abd4337d4906041324f1c9f713342112c0d24655fd295a57fe3563ac969896", - "size": 222, - "version": 1, - "type": 0, - "locktime": 0, - "vin": [ - { - "txid": "2a93fefe869367a479496484421934ed81d0e9544b42a12d579fcab9f672e7fd", - "vout": 0, - "scriptSig": { - "asm": "304402207db8b157eeb8988556a03e817fd44f24e56c2c9341c5a0e512a89607481b8cb002204e7c3831e1b522e0ef740cf31926fa0e4e6cdd6496cde5005ff29f6b44992058[ALL] 023bd027cf07a4eab516e22db16c7d77bb615bcd25a57bee6ee69dfaef23a61dac", - "hex": "47304402207db8b157eeb8988556a03e817fd44f24e56c2c9341c5a0e512a89607481b8cb002204e7c3831e1b522e0ef740cf31926fa0e4e6cdd6496cde5005ff29f6b449920580121023bd027cf07a4eab516e22db16c7d77bb615bcd25a57bee6ee69dfaef23a61dac" - }, - "sequence": 4294967295 - } - ], - "vout": [ - { - "value": 0.00040000, - "valueSat": 40000, - "n": 0, - "scriptPubKey": { - "asm": "OP_RETURN d0949bd75de50fbba5081d932fea0a0ee7407fa5", - "hex": "6a14d0949bd75de50fbba5081d932fea0a0ee7407fa5", - "type": "nulldata" - } - }, - { - "value": 0.00959776, - "valueSat": 959776, - "n": 1, - "scriptPubKey": { - "asm": "OP_DUP OP_HASH160 29f4856f82e55313c8e7bf791ac868924ec9c59b OP_EQUALVERIFY OP_CHECKSIG", - "hex": "76a91429f4856f82e55313c8e7bf791ac868924ec9c59b88ac", - "reqSigs": 1, - "type": "pubkeyhash", - "addresses": [ - "yQ9HUhd7DvVwUAXqRKWsJmgnVsVmjAq8V7" - ] - } - } - ], - "blockhash": "00000118003b266ede27cf361c3a1528f84133c42e94ab8899eba9e1398797d8", - "height": 42714, - "confirmations": 16, - "time": 1582759932, - "blocktime": 1582759932, - "instantlock": true, - "instantlock_internal": false, - "chainlock": true - } - - */ - - @Test - public void creditFundingFromWalletTest() throws UnreadableWalletException { - // recovery phrase from a wallet that was used to generate a credit funding transaction - // this is generated using the CreateWallet example in android-dashpay - String mnemonic = "odor hammer panda sunset strong fee keep demise start eagle wagon avocado"; - DerivationPathFactory factory = new DerivationPathFactory(PARAMS); - - // Create the keychain for blockchain identity funding - AuthenticationKeyChain blockchainIdentityFundingChain = AuthenticationKeyChain.authenticationBuilder() - .accountPath(factory.blockchainIdentityRegistrationFundingDerivationPath()) - .seed(new DeterministicSeed(mnemonic, null, "", Utils.currentTimeSeconds())) - .type(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING) - .build(); - - // create the authentication key chain group and add the blockchain identity funding key chain - AuthenticationKeyChainGroup group = AuthenticationKeyChainGroup.authenticationBuilder(PARAMS) - .addChain(blockchainIdentityFundingChain) - .build(); - - // Get the first key from the blockchain identity funding keychain - IDeterministicKey firstKey = group.currentKey(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING); - - /* tx data for 9788d647c020db783bd58f354eb56f049ea5f471b6a536ceb94d98b88bae4c31 */ - byte [] txData = Utils.HEX.decode("0100000001f19f93e98caea51ab67560a52b237dad7ca54546d0985aa11a1a639e9354fdba000000006a473044022074cd6796416991302d89954e458b996203428482a2d6650419ef0bba3f2a4791022054dead5e4a4eb2797628e4aecbd4be7679680293c668ae5008dc8174ec9dcd560121022535f1ee20879f1189656abd502a64c6dcbcbe5b732d4c2f780a4f6580a9f20affffffff0240420f0000000000166a14b98ac3c815ec340d9543cdc78ca2d24db1951aec20083d00000000001976a914afe755e29b119efa77f1b08c5ceacd885b53e5d088ac00000000"); - CreditFundingTransaction cftx = new CreditFundingTransaction(PARAMS, txData); - - // compare the credit burn public key id to the public key hash of the first key - assertArrayEquals(cftx.getCreditBurnPublicKeyId().getBytes(), firstKey.getPubKeyHash()); - } -} diff --git a/core/src/test/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtensionTest.java b/core/src/test/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtensionTest.java index 9228bfc64f..3670d7fc05 100644 --- a/core/src/test/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtensionTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtensionTest.java @@ -16,14 +16,15 @@ package org.bitcoinj.wallet.authentication; +import com.google.common.collect.ImmutableList; import org.bitcoinj.core.AbstractBlockChain; import org.bitcoinj.core.Address; import org.bitcoinj.core.Context; -import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Utils; import org.bitcoinj.crypto.IDeterministicKey; import org.bitcoinj.crypto.IKey; +import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.crypto.ed25519.Ed25519DeterministicKey; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.TestNet3Params; @@ -38,10 +39,10 @@ import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletExtension; import org.bitcoinj.wallet.WalletProtobufSerializer; +import org.bouncycastle.crypto.params.KeyParameter; import org.junit.Before; import org.junit.Test; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -51,6 +52,7 @@ import static org.bitcoinj.core.Utils.HEX; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -59,7 +61,7 @@ public class AuthenticationGroupExtensionTest { Wallet wallet; AuthenticationGroupExtension mnext; UnitTestParams UNITTEST = UnitTestParams.get(); - Context context;// = new Context(UNITTEST); + Context context; String seedCode = "enemy check owner stumble unaware debris suffer peanut good fabric bleak outside"; String passphrase = ""; long creationtime = 1547463771L; @@ -88,6 +90,10 @@ public void setUp() throws UnreadableWalletException { mnext.addKeyChain(seed, factory.masternodeVotingDerivationPath(), AuthenticationKeyChain.KeyChainType.MASTERNODE_VOTING); mnext.addKeyChain(seed, factory.masternodeOperatorDerivationPath(), AuthenticationKeyChain.KeyChainType.MASTERNODE_OPERATOR); mnext.addKeyChain(seed, factory.masternodePlatformDerivationPath(), AuthenticationKeyChain.KeyChainType.MASTERNODE_PLATFORM_OPERATOR); + mnext.addKeyChain(seed, factory.blockchainIdentityECDSADerivationPath(), AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY); + mnext.addKeyChain(seed, factory.blockchainIdentityRegistrationFundingDerivationPath(), AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING); + mnext.addKeyChain(seed, factory.blockchainIdentityTopupFundingDerivationPath(), AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_TOPUP); + mnext.addKeyChain(seed, factory.identityInvitationFundingDerivationPath(), AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING); wallet.addExtension(mnext); } @@ -129,6 +135,7 @@ public void roundTrip() throws UnreadableWalletException { // process the ProRegTx byte[] providerRegTxData = HEX.decode("0300010001ca9a43051750da7c5f858008f2ff7732d15691e48eb7f845c791e5dca78bab58010000006b483045022100fe8fec0b3880bcac29614348887769b0b589908e3f5ec55a6cf478a6652e736502202f30430806a6690524e4dd599ba498e5ff100dea6a872ebb89c2fd651caa71ed012103d85b25d6886f0b3b8ce1eef63b720b518fad0b8e103eba4e85b6980bfdda2dfdffffffff018e37807e090000001976a9144ee1d4e5d61ac40a13b357ac6e368997079678c888ac00000000fd1201010000000000ca9a43051750da7c5f858008f2ff7732d15691e48eb7f845c791e5dca78bab580000000000000000000000000000ffff010205064e1f3dd03f9ec192b5f275a433bfc90f468ee1a3eb4c157b10706659e25eb362b5d902d809f9160b1688e201ee6e94b40f9b5062d7074683ef05a2d5efb7793c47059c878dfad38a30fafe61575db40f05ab0a08d55119b0aad300001976a9144fbc8fb6e11e253d77e5a9c987418e89cf4a63d288ac3477990b757387cb0406168c2720acf55f83603736a314a37d01b135b873a27b411fb37e49c1ff2b8057713939a5513e6e711a71cff2e517e6224df724ed750aef1b7f9ad9ec612b4a7250232e1e400da718a9501e1d9a5565526e4b1ff68c028763"); Transaction proRegTx = new Transaction(UNITTEST, providerRegTxData); + assertTrue(mnext.isTransactionRevelant(proRegTx)); mnext.processTransaction(proRegTx, null, AbstractBlockChain.NewBlockType.BEST_CHAIN); // save and load the wallet @@ -174,11 +181,19 @@ public void keyTest() throws UnreadableWalletException { AuthenticationKeyChain.KeyChainType.MASTERNODE_PLATFORM_OPERATOR, AuthenticationKeyChain.KeyChainType.MASTERNODE_OWNER, AuthenticationKeyChain.KeyChainType.MASTERNODE_VOTING, - AuthenticationKeyChain.KeyChainType.MASTERNODE_OPERATOR)); + AuthenticationKeyChain.KeyChainType.MASTERNODE_OPERATOR, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_TOPUP, + AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING)); IDeterministicKey votingKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.MASTERNODE_VOTING).getKey(0); IDeterministicKey ownerKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.MASTERNODE_OWNER).getKey(0); IDeterministicKey operatorKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.MASTERNODE_OPERATOR).getKey(0); IDeterministicKey platformKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.MASTERNODE_PLATFORM_OPERATOR).getKey(0, true); + IDeterministicKey identityKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY).getKey(0, true); + IDeterministicKey identityAssetLockKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING).getKey(0); + IDeterministicKey topupAssetLockKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_TOPUP).getKey(0); + IDeterministicKey inviteAssetLockKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING).getKey(0); // owner keys assertArrayEquals(Utils.HEX.decode("9e2dd89b24a63cc1e8b6d63d651c4bf5f90a4acb83cd5b2028f042c5a8653d05"), ownerKey.getChainCode()); @@ -214,6 +229,62 @@ public void keyTest() throws UnreadableWalletException { assertArrayEquals(Utils.HEX.decode("c9bbba6a3ad5e87fb11af4f10458a52d3160259c"), platformKey.getPubKeyHash()); String privatePublicBase64 = ((Ed25519DeterministicKey)platformKey).getPrivatePublicBase64(); assertEquals("eJjbqnq5tVDjvvzVPcJ2d3/8iicST4MMBOF/z3S54HEI4mmP3KoK+EFpZrqTSbDI36qA7X9AlOAylYo0PkX0tg==", privatePublicBase64); + + // identity keys + assertArrayEquals(Utils.HEX.decode("4faf0c59063fb862e051b0691ced1403b93c49090333125e777801afd982523f"), identityKey.getChainCode()); + assertArrayEquals(Utils.HEX.decode("e9d761e24bfac24a226814e514ae8edb32d153c343dd9c97a4711e28374af45b"), identityKey.getPrivKeyBytes()); + assertArrayEquals(Utils.HEX.decode("0274406ba086e5cd5627cdc86f0ddd26208b01a431d2f3bc85adfebfb986427068"), identityKey.getPubKey()); + assertArrayEquals(Utils.HEX.decode("b7f3790853a273580035f5073eb32d0f6f0d8cd7"), identityKey.getPubKeyHash()); + assertEquals("XsTVA9JcVatZNZaDFQLyiU6esnSJctMk61", Address.fromKey(MainNetParams.get(), identityKey).toBase58()); + + // identity funding keys + assertArrayEquals(Utils.HEX.decode("e0e19e2f2737248d52c83da8b4975bde14c8ed0b4791b7761abe4c752df1e04d"), identityAssetLockKey.getChainCode()); + assertArrayEquals(Utils.HEX.decode("7373b7b3972f7a6aaf724f4e995848964aa64f352a1e176ef112a875d132bcc2"), identityAssetLockKey.getPrivKeyBytes()); + assertArrayEquals(Utils.HEX.decode("0385e1776ea0b87ffbc22ddfb43b23bb55d172589f7d3658d81d3031409f952c54"), identityAssetLockKey.getPubKey()); + assertArrayEquals(Utils.HEX.decode("e02011931ed41d1d723e1e6499eb08c1a33cfad5"), identityAssetLockKey.getPubKeyHash()); + assertEquals("Xw7ucPiM7wsWFbQaj4W4DmXqtceJGPhA92", Address.fromKey(MainNetParams.get(), identityAssetLockKey).toBase58()); + + // topup funding keys + assertArrayEquals(Utils.HEX.decode("2ee94cdfc5dba90ceb6dd89e79fce8a949fbdf8daedfe25889c3d5a287930662"), topupAssetLockKey.getChainCode()); + assertArrayEquals(Utils.HEX.decode("e54482d56dd010d166c933af6a45424be897d9ea1079da2ab8238fad9e25fc4b"), topupAssetLockKey.getPrivKeyBytes()); + assertArrayEquals(Utils.HEX.decode("03e674116b1f9b79fe0f84c5668c195d2f84bce98f549a3b7e5e2b3019d4975673"), topupAssetLockKey.getPubKey()); + assertArrayEquals(Utils.HEX.decode("68498b3f25500320becdfd0c7867d7dcc79903eb"), topupAssetLockKey.getPubKeyHash()); + assertEquals("XkCGD8FX7HMQ1yYnHswNMikHu4sLJgVfWG", Address.fromKey(MainNetParams.get(), topupAssetLockKey).toBase58()); + + // invite funding keys + assertArrayEquals(Utils.HEX.decode("89deb042babf138e2e3c08dae5612d3f24464642af13159547fe6665d0217457"), inviteAssetLockKey.getChainCode()); + assertArrayEquals(Utils.HEX.decode("f176cb92188a8fa6364c3f5459ea2513d424d24c4707685a355f0a60c9a4c288"), inviteAssetLockKey.getPrivKeyBytes()); + assertArrayEquals(Utils.HEX.decode("03c6c4b06e4b0899d8e80f8ccb24a6294277d913835cbd6615ea665b8969535e1e"), inviteAssetLockKey.getPubKey()); + assertArrayEquals(Utils.HEX.decode("b347814060b91175e609b5262fcc83a5c722110c"), inviteAssetLockKey.getPubKeyHash()); + assertEquals("Xs2nSqYpwb7cvyWikp5j8LHwEVo7ovCp96", Address.fromKey(MainNetParams.get(), inviteAssetLockKey).toBase58()); + } + + @Test + public void encryptedSeedTest() throws UnreadableWalletException { + KeyCrypterScrypt keyCrypterScrypt = new KeyCrypterScrypt(16); + KeyParameter aesKey = keyCrypterScrypt.deriveKey("password"); + DeterministicSeed seed = new DeterministicSeed(seedCode, null, passphrase, creationtime); + seed = seed.encrypt(keyCrypterScrypt, aesKey); + + wallet.encrypt(keyCrypterScrypt, aesKey); + + AuthenticationGroupExtension mnext = new AuthenticationGroupExtension(wallet); + mnext.addEncryptedKeyChains(MainNetParams.get(), seed, aesKey, EnumSet.of( + AuthenticationKeyChain.KeyChainType.MASTERNODE_PLATFORM_OPERATOR, + AuthenticationKeyChain.KeyChainType.MASTERNODE_OWNER, + AuthenticationKeyChain.KeyChainType.MASTERNODE_VOTING, + AuthenticationKeyChain.KeyChainType.MASTERNODE_OPERATOR, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_TOPUP, + AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING)); + + // owner keys + IDeterministicKey ownerKey = mnext.getKeyChain(AuthenticationKeyChain.KeyChainType.MASTERNODE_OWNER).getKey(0); + assertArrayEquals(Utils.HEX.decode("9e2dd89b24a63cc1e8b6d63d651c4bf5f90a4acb83cd5b2028f042c5a8653d05"), ownerKey.getChainCode()); + assertArrayEquals(Utils.HEX.decode("03877e1b244bea3d8428bb1d295b1a17a7970ed0c11b735b07536ef2e48b77c0fc"), ownerKey.getPubKey()); + assertArrayEquals(Utils.HEX.decode("9660052877ef6f3b5d1ed740b594a67f351352f3"), ownerKey.getPubKeyHash()); + assertEquals("XpPxDdCV5is57YvZyDejDchDH4GjUHrBc8", Address.fromKey(MainNetParams.get(), ownerKey).toBase58()); } @Test @@ -235,4 +306,26 @@ public void loadWallet() throws IOException, UnreadableWalletException { assertEquals(usageBefore, usageAfter); } } + + @Test + public void missingChainTypesTest() { + assertFalse(mnext.missingAnyKeyChainTypes(EnumSet.of(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING))); + assertFalse(mnext.missingAnyKeyChainTypes(EnumSet.of(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY, + AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING, + AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_TOPUP))); + assertTrue(mnext.missingAnyKeyChainTypes(EnumSet.of(AuthenticationKeyChain.KeyChainType.INVALID_KEY_CHAIN))); + } + + @Test + public void hasKeyChainTest() { + assertFalse(mnext.hasKeyChain(ImmutableList.of())); // path: m + assertFalse(mnext.hasKeyChain(DerivationPathFactory.get(UNITTEST).masternodeHoldingsDerivationPath())); + assertTrue(mnext.hasKeyChain(DerivationPathFactory.get(UNITTEST).masternodeOperatorDerivationPath())); + } + + @Test + public void supportsBloomFiltersTest() { + assertTrue(mnext.supportsBloomFilters()); + } } diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index 6761dec7a2..04e89f7ae0 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -285,6 +285,7 @@ public static void main(String[] args) throws Exception { parser.accepts("locktime").withRequiredArg(); parser.accepts("allow-unconfirmed"); parser.accepts("coinjoin"); + parser.accepts("return-change"); parser.accepts("offline"); parser.accepts("ignore-mandatory-extensions"); OptionSpec passwordFlag = parser.accepts("password").withRequiredArg(); @@ -447,7 +448,8 @@ public static void main(String[] args) throws Exception { } boolean allowUnconfirmed = options.has("allow-unconfirmed"); boolean isCoinJoin = options.has("coinjoin"); - send(outputFlag.values(options), feePerKb, lockTime, allowUnconfirmed, isCoinJoin); + boolean returnChange = options.has("return-change"); + send(outputFlag.values(options), feePerKb, lockTime, allowUnconfirmed, isCoinJoin, returnChange); } else if (options.has(paymentRequestLocation)) { sendPaymentRequest(paymentRequestLocation.value(options), !options.has("no-pki")); } else { @@ -692,7 +694,7 @@ private static void addAddr() { } } - private static void send(List outputs, Coin feePerKb, String lockTimeStr, boolean allowUnconfirmed, boolean isCoinJoin) throws VerificationException { + private static void send(List outputs, Coin feePerKb, String lockTimeStr, boolean allowUnconfirmed, boolean isCoinJoin, boolean returnChange) throws VerificationException { try { // Convert the input strings to outputs. Transaction t = new Transaction(params); @@ -718,7 +720,7 @@ private static void send(List outputs, Coin feePerKb, String lockTimeStr return; } } - SendRequest req = isCoinJoin ? CoinJoinSendRequest.forTx(wallet, t) : SendRequest.forTx(t); + SendRequest req = isCoinJoin ? CoinJoinSendRequest.forTx(wallet, t, returnChange) : SendRequest.forTx(t); if (!isCoinJoin) req.coinSelector = UnmixedZeroConfCoinSelector.get(); if (t.getOutputs().size() == 1 && t.getOutput(0).getValue().equals(isCoinJoin ? wallet.getBalance(BalanceType.COINJOIN) :wallet.getBalance())) { @@ -1351,7 +1353,9 @@ private static void setup() throws BlockStoreException { } else { //TODO: peerGroup.setRequiredServices(0); was used here previously, which is better //for now, however peerGroup doesn't work with masternodeListManager, but it should - peerGroup.addPeerDiscovery(new ThreeMethodPeerDiscovery(params, Context.get().masternodeListManager)); + ThreeMethodPeerDiscovery peerDiscovery = new ThreeMethodPeerDiscovery(params, Context.get().masternodeListManager); + peerDiscovery.setIncludeDNS(false); + peerGroup.addPeerDiscovery(peerDiscovery); } }