Skip to content

Commit

Permalink
refactor: improve AssetLockTransaction for multiple credit outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
HashEngineering committed Jan 10, 2024
1 parent 6b51275 commit 7442e57
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 71 deletions.
18 changes: 9 additions & 9 deletions core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,22 @@


import com.google.common.collect.Lists;
import org.bitcoinj.core.Address;
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.bitcoinj.script.ScriptPattern;
import org.json.JSONObject;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.concurrent.atomic.AtomicReference;

import static org.bitcoinj.core.Transaction.Type.TRANSACTION_ASSET_LOCK;
import static org.bitcoinj.core.Utils.HEX;

public class AssetLockPayload extends SpecialTxPayload {

Expand Down Expand Up @@ -118,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
153 changes: 95 additions & 58 deletions core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@
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
Expand All @@ -33,15 +39,11 @@
*/
public class AssetLockTransaction extends Transaction {

TransactionOutput lockedOutput;
TransactionOutPoint lockedOutpoint;
Coin fundingAmount;
Sha256Hash identityId;
ECKey assetLockPublicKey;
KeyId assetLockPublicKeyId;
// in memory
int usedDerivationPathIndex;
AssetLockPayload assetLockPayload;
private ArrayList<TransactionOutPoint> lockedOutpoints;
private ArrayList<Sha256Hash> identityIds;
private TreeMap<Integer, ECKey> assetLockPublicKeys;
private ArrayList<KeyId> assetLockPublicKeyIds;
private AssetLockPayload assetLockPayload;



Expand All @@ -60,28 +62,28 @@ public AssetLockTransaction(Transaction tx) {
}

/**
* Creates a credit funding transaction.
* 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.fundingAmount = fundingAmount;
this.assetLockPublicKey = assetLockPublicKey;
this.assetLockPublicKeyId = KeyId.fromBytes(this.assetLockPublicKey.getPubKeyHash());
this.identityId = Sha256Hash.ZERO_HASH;
if (assetLockPublicKey instanceof DeterministicKey) {
this.usedDerivationPathIndex = ((DeterministicKey)assetLockPublicKey).getChildNumber().num();
} else this.usedDerivationPathIndex = -1;
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, this.assetLockPublicKey));
TransactionOutput realOutput = new TransactionOutput(params, this, fundingAmount, Address.fromKey(params, assetLockPublicKey));

lockedOutput = new TransactionOutput(params, null, fundingAmount, ScriptBuilder.createAssetLockOutput().getProgram());
lockedOutpoints = Lists.newArrayList();
TransactionOutput assetLockOutput = new TransactionOutput(params, null, fundingAmount, ScriptBuilder.createAssetLockOutput().getProgram());
assetLockPayload = new AssetLockPayload(params, Lists.newArrayList(realOutput));
setExtraPayload(assetLockPayload);
addOutput(lockedOutput);
addOutput(assetLockOutput);
}

/**
Expand All @@ -105,85 +107,120 @@ protected void parse() throws ProtocolException {
@Override
protected void unCache() {
super.unCache();
lockedOutpoint = null;
identityId = Sha256Hash.ZERO_HASH;
fundingAmount = Coin.ZERO;
assetLockPublicKeyId = KeyId.KEYID_ZERO;
lockedOutpoints.clear();
identityIds.clear();
assetLockPublicKeyIds.clear();
}

/**
* Initializes lockedOutput, lockedOutpoint, fundingAmount and the hash160
* credit burn key
* Initializes lockedOutpoints and the hash160
* assetlock key
*/
private void parseTransaction() {
assetLockPayload = (AssetLockPayload) getExtraPayloadObject();
getLockedOutput();
lockedOutpoints = Lists.newArrayList();
assetLockPublicKeyIds = Lists.newArrayList();
assetLockPublicKeys = Maps.newTreeMap();
identityIds = Lists.newArrayList();
getLockedOutpoint();
fundingAmount = lockedOutput.getValue();
getAssetLockPublicKeyId();
identityId = getIdentityId();
getIdentityId();
}

/**
* Sets lockedOutput and returns output that has the OP_RETURN script
*/

public TransactionOutput getLockedOutput() {
if(lockedOutput != null)
return lockedOutput;
lockedOutput = assetLockPayload.getCreditOutputs().get(0);
return lockedOutput;
return getLockedOutput(0);

Check warning on line 135 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L135

Added line #L135 was not covered by tests
}

public TransactionOutput getLockedOutput(int outputIndex) {
return assetLockPayload.getCreditOutputs().get(outputIndex);

Check warning on line 139 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L139

Added line #L139 was not covered by tests
}

public TransactionOutPoint getLockedOutpoint() {
return getLockedOutpoint(0);
}

public AssetLockPayload getAssetLockPayload() {
return assetLockPayload;

Check warning on line 147 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L147

Added line #L147 was not covered by tests
}

/**
* Sets lockedOutpoint and returns outpoint that has the OP_RETURN script
*/
public TransactionOutPoint getLockedOutpoint() {
if(lockedOutpoint != null)
return lockedOutpoint;

lockedOutpoint = new TransactionOutPoint(params, 0, Sha256Hash.wrap(getTxId().getReversedBytes()));

return lockedOutpoint;

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 fundingAmount;
return assetLockPayload.getFundingAmount();
}

/**
* Returns the credit burn identifier, which is the sha256(sha256(outpoint))
*/
public Sha256Hash getIdentityId() {
if(identityId == null || identityId.isZero()) {
try {
ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(36);
getLockedOutpoint().bitcoinSerialize(bos);
identityId = Sha256Hash.twiceOf(bos.toByteArray());
} catch (IOException x) {
throw new RuntimeException(x);
}
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);

Check warning on line 185 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L184-L185

Added lines #L184 - L185 were not covered by tests
}
});
}
return identityId;
return identityIds.get(0);
}

public ECKey getAssetLockPublicKey() {
return assetLockPublicKey;
return getAssetLockPublicKey(0);

Check warning on line 193 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L193

Added line #L193 was not covered by tests
}

public ECKey getAssetLockPublicKey(int outputIndex) {
return assetLockPublicKeys.get(outputIndex);

Check warning on line 197 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L197

Added line #L197 was not covered by tests
}


public KeyId getAssetLockPublicKeyId() {
if(assetLockPublicKeyId == null || assetLockPublicKeyId.equals(KeyId.KEYID_ZERO)) {
assetLockPublicKeyId = KeyId.fromBytes(ScriptPattern.extractHashFromP2PKH(assetLockPayload.getCreditOutputs().get(0).getScriptPubKey()));
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 assetLockPublicKeyId;
return assetLockPublicKeyIds.get(outputIndex);
}

public int getUsedDerivationPathIndex() {
return usedDerivationPathIndex;
public int getUsedDerivationPathIndex(int outputIndex) {
ECKey key = getAssetLockPublicKey(0);

Check warning on line 212 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L212

Added line #L212 was not covered by tests
if (key instanceof IDeterministicKey) {
IDeterministicKey deterministicKey = (IDeterministicKey) key;
return deterministicKey.getPath().get(deterministicKey.getDepth() - 1).num();

Check warning on line 215 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L214-L215

Added lines #L214 - L215 were not covered by tests
}
return -1;

Check warning on line 217 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L217

Added line #L217 was not covered by tests
}

public void setAssetLockPublicKeyAndIndex(ECKey assetLockPublicKey, int usedDerivationPathIndex) {
this.assetLockPublicKey = assetLockPublicKey;
this.usedDerivationPathIndex = usedDerivationPathIndex;
public void setAssetLockPublicKey(ECKey assetLockPublicKey) {
int index = assetLockPublicKeyIds.indexOf(KeyId.fromBytes(assetLockPublicKey.getPubKeyHash()));

Check warning on line 221 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L221

Added line #L221 was not covered by tests
checkState(index != -1, "cannot find public key hash for " + assetLockPublicKey);
assetLockPublicKeys.put(index, assetLockPublicKey);
}

Check warning on line 224 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L223-L224

Added lines #L223 - L224 were not covered by tests

/**
Expand All @@ -199,7 +236,7 @@ public static boolean isAssetLockTransaction(Transaction tx) {
* Determines the first output that is a credit burn output
* or returns -1.
*/
public long getOutputIndex() {
public long getAssetLockOutputIndex() {
int outputCount = getOutputs().size();

Check warning on line 240 in core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/evolution/AssetLockTransaction.java#L240

Added line #L240 was not covered by tests
for (int i = 0; i < outputCount; ++i) {
if (ScriptPattern.isAssetLock(getOutput(i).getScriptPubKey()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ public AssetLockTransaction getAssetLockTransaction(Transaction tx) {
publicKey = (DeterministicKey) getInvitationFundingKeyChain().getKeyByPubKeyHash(cftx.getAssetLockPublicKeyId().getBytes());

Check warning on line 737 in core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java#L737

Added line #L737 was not covered by tests

if(publicKey != null)
cftx.setAssetLockPublicKeyAndIndex(publicKey, publicKey.getChildNumber().num());
cftx.setAssetLockPublicKey(publicKey);
else log.error("Cannot find " + KeyId.fromBytes(cftx.getAssetLockPublicKeyId().getBytes()) + " in the wallet");

Check warning on line 741 in core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/org/bitcoinj/wallet/authentication/AuthenticationGroupExtension.java#L740-L741

Added lines #L740 - L741 were not covered by tests

mapCreditFundingTxs.put(cftx.getTxId(), cftx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ public void startup() {
public void testIdentityAssetLockTransactionUniqueId() {
AssetLockTransaction fundingTransaction = new AssetLockTransaction(PARAMS, transactionData);

String lockedOutpoint = fundingTransaction.lockedOutpoint.toStringBase64();
String lockedOutpoint = fundingTransaction.getLockedOutpoint().toStringBase64();
assertEquals("Locked outpoint is incorrect", "nQxmWXkiwGZ6VzBk72ukhBKuTZH/AZgfx8pAvPbVbPIAAAAA", lockedOutpoint);
String identityIdentifier = fundingTransaction.identityId.toStringBase58();
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.assetLockPublicKeyId.getBytes());
assertArrayEquals("The private key doesn't match the funding transaction", privateKey.getPubKeyHash(), fundingTransaction.getAssetLockPublicKeyId().getBytes());

}

Expand Down

0 comments on commit 7442e57

Please sign in to comment.