diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index 9b706cec5..29b2fb33d 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -107,6 +107,9 @@ public enum Type { TRANSACTION_PROVIDER_UPDATE_REVOKE(4), TRANSACTION_COINBASE(5), TRANSACTION_QUORUM_COMMITMENT(6), + TRANSACTION_ASSET_LOCK(8), + TRANSACTION_ASSET_UNLOCK(9), + TRANSACTION_TYPE_MAX(10), TRANSACTION_UNKNOWN(1024); final int value; @@ -1704,7 +1707,15 @@ protected void setExtraPayloadObject() { case TRANSACTION_QUORUM_COMMITMENT: extraPayloadObject = new FinalCommitmentTxPayload(params, this); break; + case TRANSACTION_ASSET_LOCK: + extraPayloadObject = new AssetLockPayload(params, this); + break; + case TRANSACTION_ASSET_UNLOCK: + extraPayloadObject = new AssetUnlockPayload(params, this); + break; } + if (extraPayloadObject != null) + extraPayloadObject.setParent(this); } /* returns false if inputs > 4 or there are less than the required confirmations */ @@ -1723,6 +1734,7 @@ public boolean isSimple() { public boolean requiresInputs() { switch (getType()) { case TRANSACTION_QUORUM_COMMITMENT: + case TRANSACTION_ASSET_UNLOCK: return false; default: return true; diff --git a/core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java b/core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java new file mode 100644 index 000000000..ff6e40aef --- /dev/null +++ b/core/src/main/java/org/bitcoinj/evolution/AssetLockPayload.java @@ -0,0 +1,102 @@ +/* + * Copyright 2023 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 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.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.bitcoinj.core.Transaction.Type.TRANSACTION_ASSET_LOCK; + +public class AssetLockPayload extends SpecialTxPayload { + + public static final int CURRENT_VERSION = 1; + public static final Transaction.Type SPECIALTX_TYPE = TRANSACTION_ASSET_LOCK; + private ArrayList creditOutputs; + + public AssetLockPayload(NetworkParameters params, Transaction tx) { + super(params, tx); + } + + public AssetLockPayload(NetworkParameters params, List creditOutputs) { + this(params, CURRENT_VERSION, creditOutputs); + } + + public AssetLockPayload(NetworkParameters params, int version, List creditOutputs) { + super(params, version); + this.creditOutputs = new ArrayList<>(creditOutputs); + length = new VarInt(creditOutputs.size()).getSizeInBytes(); + creditOutputs.forEach(output -> length += output.getMessageSize()); + } + @Override + protected void parse() throws ProtocolException { + super.parse(); + int size = (int) readVarInt(); + creditOutputs = Lists.newArrayList(); + for (int i = 0; i < size; ++i) { + TransactionOutput output = new TransactionOutput(params, null, payload, cursor); + cursor += output.getMessageSize(); + creditOutputs.add(output); + } + length = cursor - offset; + } + + @Override + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + super.bitcoinSerializeToStream(stream); + stream.write(new VarInt(creditOutputs.size()).encode()); + for (int i = 0; i < creditOutputs.size(); ++i) { + creditOutputs.get(i).bitcoinSerialize(stream); + } + } + + public int getCurrentVersion() { + return CURRENT_VERSION; + } + + public String toString() { + return String.format("AssetLockPayload(creditOutputs: %d)", + creditOutputs.size()); + } + + @Override + public Transaction.Type getType() { + return Transaction.Type.TRANSACTION_ASSET_UNLOCK; + } + + @Override + public String getName() { + return "AssetLock"; + } + + @Override + public JSONObject toJson() { + JSONObject result = super.toJson(); + creditOutputs.forEach(output -> result.append("creditOutputs", output.toString())); + return result; + } +} diff --git a/core/src/main/java/org/bitcoinj/evolution/AssetUnlockPayload.java b/core/src/main/java/org/bitcoinj/evolution/AssetUnlockPayload.java new file mode 100644 index 000000000..9fb5bc981 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/evolution/AssetUnlockPayload.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 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.NetworkParameters; +import org.bitcoinj.core.ProtocolException; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Utils; +import org.bitcoinj.crypto.BLSLazySignature; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; + +import static org.bitcoinj.core.Transaction.Type.TRANSACTION_ASSET_UNLOCK; + +public class AssetUnlockPayload extends SpecialTxPayload { + + public static final int CURRENT_VERSION = 1; + public static final Transaction.Type SPECIALTX_TYPE = TRANSACTION_ASSET_UNLOCK; + + private long index; + private long fee; + private long requestedHeight; + private Sha256Hash quorumHash; + BLSLazySignature quorumSig; + public AssetUnlockPayload(NetworkParameters params, Transaction tx) { + super(params, tx); + } + + + public AssetUnlockPayload(NetworkParameters params, int version, long index, long fee, int requestedHeight, Sha256Hash quorumHash, BLSLazySignature quorumSig) { + super(params, version); + this.index = index; + this.fee = fee; + this.requestedHeight = requestedHeight; + this.quorumHash = quorumHash; + this.quorumSig = quorumSig; + length = 8 + 4 + 4 + 32 + quorumSig.getMessageSize(); + } + + @Override + protected void parse() throws ProtocolException { + super.parse(); + index = readInt64(); + fee = readUint32(); + requestedHeight = readUint32(); + quorumHash = readHash(); + quorumSig = new BLSLazySignature(params, payload, cursor); + cursor += quorumSig.getMessageSize(); + length = cursor - offset; + } + + @Override + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + super.bitcoinSerializeToStream(stream); + Utils.uint64ToByteStreamLE(BigInteger.valueOf(index), stream); + Utils.uint32ToByteStreamLE(fee, stream); + Utils.uint32ToByteStreamLE(requestedHeight, stream); + stream.write(quorumHash.getReversedBytes()); + quorumSig.bitcoinSerialize(stream); + } + + public int getCurrentVersion() { + return CURRENT_VERSION; + } + + public String toString() { + return String.format("AssetUnlockPayload(index: %d, fee: %d, requestedHeight: %d, quorumHash: %s, quorumSig: %s)", + index, fee, requestedHeight, quorumHash, quorumSig); + } + + @Override + public Transaction.Type getType() { + return TRANSACTION_ASSET_UNLOCK; + } + + @Override + public String getName() { + return "AssetUnlock"; + } + + @Override + public JSONObject toJson() { + JSONObject result = super.toJson(); + result.put("index", index); + result.put("fee", fee); + result.put("requestedHeight", requestedHeight); + result.put("quorumHash", quorumHash); + result.put("quorumSig", quorumSig); + return result; + } +} diff --git a/core/src/test/java/org/bitcoinj/evolution/AssetLockTest.java b/core/src/test/java/org/bitcoinj/evolution/AssetLockTest.java new file mode 100644 index 000000000..d9087e782 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/evolution/AssetLockTest.java @@ -0,0 +1,118 @@ +package org.bitcoinj.evolution; + +import com.google.common.collect.Lists; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.crypto.BLSLazySignature; +import org.bitcoinj.params.UnitTestParams; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.wallet.DefaultRiskAnalysis; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; + +import static org.bitcoinj.core.Coin.CENT; +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(); + ArrayList dummyTransactions; + ArrayList dummyOutputs; + + @Before + public void setupDummyInputs() + { + dummyTransactions = Lists.newArrayListWithCapacity(2); + dummyTransactions.add(new Transaction(PARAMS)); + dummyTransactions.add(new Transaction(PARAMS)); + dummyOutputs = Lists.newArrayListWithCapacity(4); + // Add some keys to the keystore: + ECKey [] key = new ECKey[] { + new ECKey(), new ECKey(), new ECKey(), new ECKey() + }; + + dummyTransactions.get(0).addOutput(CENT.multiply(11), ScriptBuilder.createP2PKOutputScript(key[0])); + dummyTransactions.get(0).addOutput(CENT.multiply(50), ScriptBuilder.createP2PKOutputScript(key[1])); + dummyOutputs.addAll(dummyTransactions.get(0).getOutputs()); + + + dummyTransactions.get(0).addOutput(CENT.multiply(21), ScriptBuilder.createP2PKOutputScript(key[2])); + dummyTransactions.get(0).addOutput(CENT.multiply(22), ScriptBuilder.createP2PKOutputScript(key[3])); + dummyOutputs.addAll(dummyTransactions.get(1).getOutputs()); + } + + private Transaction createAssetLockTx(ECKey key) { + + ArrayList creditOutputs = Lists.newArrayListWithCapacity(2); + creditOutputs.add(new TransactionOutput(PARAMS, null, CENT.multiply(17), ScriptBuilder.createP2PKOutputScript(key).getProgram())); + creditOutputs.add(new TransactionOutput(PARAMS, null, CENT.multiply(13), ScriptBuilder.createP2PKOutputScript(key).getProgram())); + + AssetLockPayload assetLockTx = new AssetLockPayload(PARAMS, 1, creditOutputs); + + Transaction tx = new Transaction(PARAMS); + tx.setVersionAndType(3, Transaction.Type.TRANSACTION_ASSET_LOCK); + tx.setExtraPayload(assetLockTx); + tx.addInput(dummyTransactions.get(0).getTxId(), 1, new Script(new byte[65])); + tx.addOutput(CENT.multiply(30), ScriptBuilder.createOpReturnScript(new byte[0])); + tx.addOutput(CENT.multiply(20), ScriptBuilder.createP2PKOutputScript(key)); + return tx; + } + + private Transaction createAssetUnlockTx(ECKey key) + { + int version = 1; + // just a big number bigger than uint32_t + long index = 0x001122334455667788L; + // big enough to overflow int32_t + int fee = 2000000000; + // just big enough to overflow uint16_t + int requestedHeight = 1000000; + Sha256Hash quorumHash = Sha256Hash.ZERO_HASH; + BLSLazySignature quorumSig = new BLSLazySignature(PARAMS); + AssetUnlockPayload assetUnlockTx = new AssetUnlockPayload(PARAMS, version, index, fee, requestedHeight, quorumHash, quorumSig); + + Transaction tx = new Transaction(PARAMS); + tx.setVersionAndType(3, Transaction.Type.TRANSACTION_ASSET_UNLOCK); + tx.setExtraPayload(assetUnlockTx); + + tx.addOutput(CENT.multiply(10), ScriptBuilder.createP2PKOutputScript(key)); + tx.addOutput(CENT.multiply(20), ScriptBuilder.createP2PKOutputScript(key)); + + return tx; + } + + @Test + public void assetLock() { + ECKey key = new ECKey(); + + Transaction tx = createAssetLockTx(key); + assertSame(DefaultRiskAnalysis.RuleViolation.NONE, DefaultRiskAnalysis.isStandard(tx)); + + tx.verify(); + assertTrue(tx.getInputs().stream().allMatch(input -> DefaultRiskAnalysis.isInputStandard(input) == DefaultRiskAnalysis.RuleViolation.NONE)); + + // Check version + assertEquals(3, tx.getVersionShort()); + AssetLockPayload lockPayload = (AssetLockPayload) tx.getExtraPayloadObject(); + assertEquals(1, lockPayload.getVersion()); + } + + @Test + public void assetUnlock() { + ECKey key = new ECKey(); + final Transaction tx = createAssetUnlockTx(key); + assertSame(DefaultRiskAnalysis.RuleViolation.NONE, DefaultRiskAnalysis.isStandard(tx)); + tx.verify(); + + // Check version + assertEquals(3, tx.getVersionShort()); + AssetLockPayload lockPayload = (AssetLockPayload) tx.getExtraPayloadObject(); + assertEquals(1, lockPayload.getVersion()); + } +}