Skip to content

Commit

Permalink
feat(coinjoin): improve asset lock handling and other fixes (#244)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
HashEngineering authored Jan 16, 2024
1 parent eb4e227 commit f6cd23d
Show file tree
Hide file tree
Showing 20 changed files with 753 additions and 512 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static SendRequest to(Wallet wallet, Address destination, Coin value) {
}

/**
* <p>Creates a new SCoinJoin endRequest to the given pubkey for the given value.</p>
* <p>Creates a new CoinJoin SendRequest to the given pubkey for the given value.</p>
*/
public static SendRequest to(Wallet wallet, ECKey destination, Coin value) {
SendRequest req = SendRequest.to(wallet.getParams(), destination, value);
Expand All @@ -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;
}
}
4 changes: 2 additions & 2 deletions core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
24 changes: 14 additions & 10 deletions core/src/main/java/org/bitcoinj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
Expand All @@ -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();
}

Expand Down
25 changes: 23 additions & 2 deletions core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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() : "<no scriptPubKey>");
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
Expand All @@ -97,6 +111,13 @@ public List<TransactionOutput> 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();
Expand Down
247 changes: 247 additions & 0 deletions core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java
Original file line number Diff line number Diff line change
@@ -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<TransactionOutPoint> lockedOutpoints;
private ArrayList<Sha256Hash> identityIds;
private TreeMap<Integer, ECKey> assetLockPublicKeys;
private ArrayList<KeyId> 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;
}
}
Loading

0 comments on commit f6cd23d

Please sign in to comment.