One transactions, two wallets
import org.junit.Before | |
import org.junit.Test | |
import kotlin.test.fail | |
import org.bitcoinj.core.Transaction | |
import org.bitcoinj.params.MainNetParams | |
import org.bitcoinj.core.NetworkParameters | |
import org.bitcoinj.core.ECKey | |
import org.bitcoinj.core.Address | |
import org.bitcoinj.testing.FakeTxBuilder | |
import org.bitcoinj.core.Coin | |
import org.bitcoinj.core.Wallet | |
import org.bitcoinj.core.AbstractBlockChain | |
import org.bitcoinj.core.VerificationException | |
import org.bitcoinj.store.MemoryBlockStore | |
import org.bitcoinj.params.RegTestParams | |
import org.bitcoinj.core.TransactionInput | |
import org.bitcoinj.core.ScriptException | |
import org.bitcoinj.script.Script | |
import org.bitcoinj.wallet.RedeemData | |
import com.google.common.base.Preconditions | |
import org.bitcoinj.signers.TransactionSigner | |
import org.bitcoinj.core.ECKey | |
import org.bitcoinj.core.ScriptException | |
import org.bitcoinj.core.Transaction | |
import org.bitcoinj.core.TransactionInput | |
import org.bitcoinj.crypto.DeterministicKey | |
import org.bitcoinj.crypto.TransactionSignature | |
import org.bitcoinj.script.Script | |
import org.bitcoinj.wallet.KeyBag | |
import org.bitcoinj.wallet.RedeemData | |
import org.slf4j.Logger | |
import org.slf4j.LoggerFactory | |
import org.bitcoinj.signers.StatelessTransactionSigner | |
import org.bitcoinj.core.Utils | |
/** | |
* Created by devrandom on 2015-02-24. | |
*/ | |
private val log = LoggerFactory.getLogger("test_atomic") | |
public class LenientTransactionSigner : StatelessTransactionSigner() { | |
override fun isReady(): Boolean { | |
return true | |
} | |
override fun signInputs(propTx: TransactionSigner.ProposedTransaction, keyBag: KeyBag): Boolean { | |
val tx = propTx.partialTx | |
val numInputs = tx.getInputs().size() | |
for (i in 0..numInputs - 1) { | |
val txIn = tx.getInput(i.toLong()) | |
if (txIn.getConnectedOutput() == null) { | |
log.warn("Missing connected output, assuming input {} is already signed.", i) | |
continue | |
} | |
try { | |
// We assume if its already signed, its hopefully got a SIGHASH type that will not invalidate when | |
// we sign missing pieces (to check this would require either assuming any signatures are signing | |
// standard output types or a way to get processed signatures out of script execution) | |
txIn.getScriptSig().correctlySpends(tx, i.toLong(), txIn.getConnectedOutput()!!.getScriptPubKey()) | |
log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i) | |
continue | |
} catch (e: ScriptException) { | |
// Expected. | |
} | |
val redeemData = txIn.getConnectedRedeemData(keyBag) | |
if (redeemData == null) | |
continue | |
val scriptPubKey = txIn.getConnectedOutput()!!.getScriptPubKey() | |
// For P2SH inputs we need to share derivation path of the signing key with other signers, so that they | |
// use correct key to calculate their signatures. | |
// Married keys all have the same derivation path, so we can safely just take first one here. | |
val pubKey = redeemData.keys.get(0) | |
if (pubKey is DeterministicKey) | |
propTx.keyPaths.put(scriptPubKey, ((pubKey as DeterministicKey).getPath())) | |
val key: ECKey | |
// locate private key in redeem data. For pay-to-address and pay-to-key inputs RedeemData will always contain | |
// only one key (with private bytes). For P2SH inputs RedeemData will contain multiple keys, one of which MAY | |
// have private bytes | |
key = redeemData.getFullKey() | |
if (key == null) { | |
log.warn("No local key found for input {}", i) | |
continue | |
} | |
var inputScript = txIn.getScriptSig() | |
// script here would be either a standard CHECKSIG program for pay-to-address or pay-to-pubkey inputs or | |
// a CHECKMULTISIG program for P2SH inputs | |
val script = redeemData.redeemScript.getProgram() | |
try { | |
val signature = tx.calculateSignature(i, key, script, Transaction.SigHash.ALL, false) | |
// at this point we have incomplete inputScript with OP_0 in place of one or more signatures. We already | |
// have calculated the signature using the local key and now need to insert it in the correct place | |
// within inputScript. For pay-to-address and pay-to-key script there is only one signature and it always | |
// goes first in an inputScript (sigIndex = 0). In P2SH input scripts we need to figure out our relative | |
// position relative to other signers. Since we don't have that information at this point, and since | |
// we always run first, we have to depend on the other signers rearranging the signatures as needed. | |
// Therefore, always place as first signature. | |
val sigIndex = 0 | |
inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), sigIndex) | |
txIn.setScriptSig(inputScript) | |
} catch (e: ECKey.KeyIsEncryptedException) { | |
throw e | |
} catch (e: ECKey.MissingPrivateKeyException) { | |
log.warn("No private key in keypair for input {}", i) | |
} | |
} | |
return true | |
} | |
} | |
class AtomicSwapTest() { | |
var wallet1: Wallet? = null | |
var wallet2: Wallet? = null | |
val params: NetworkParameters = RegTestParams.get() | |
val blockStore = MemoryBlockStore(params) | |
Before fun setUp() { | |
wallet1 = Wallet(params) | |
wallet2 = Wallet(params) | |
sendMoneyToWallet(wallet1!!, FakeTxBuilder.createFakeTx(params, Coin.FIFTY_COINS, wallet1!!.currentReceiveAddress())) | |
sendMoneyToWallet(wallet2!!, FakeTxBuilder.createFakeTx(params, Coin.FIFTY_COINS, wallet2!!.currentReceiveAddress())) | |
} | |
fun sendMoneyToWallet(wallet: Wallet, tx: Transaction){ | |
var bp = FakeTxBuilder.createFakeBlock(blockStore, tx); | |
wallet.receiveFromBlock(tx, bp.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0); | |
wallet.notifyNewBestBlock(bp.storedBlock); | |
} | |
Test fun testAtomic() { | |
val s1 = ECKey() | |
val s2 = ECKey() | |
val destKey = ECKey() | |
val dest = destKey.toAddress(params) | |
val req1 = Wallet.SendRequest.to(dest, Coin.CENT) | |
val req2 = Wallet.SendRequest.to(dest, Coin.COIN) | |
req1.signInputs = false | |
req2.signInputs = false | |
wallet1!!.completeTx(req1) | |
wallet2!!.completeTx(req2) | |
val tx = Transaction(params) | |
req1.tx.getInputs().forEach { tx.addInput(it) } | |
req2.tx.getInputs().forEach { tx.addInput(it) } | |
req1.tx.getOutputs().forEach { tx.addOutput(it) } | |
req2.tx.getOutputs().forEach { tx.addOutput(it) } | |
val req = Wallet.SendRequest.forTx(tx) | |
req.signInputs = false | |
wallet2!!.completeTx(req) | |
val numInputs = tx.getInputs().size() | |
for (i in 0L..numInputs - 1) { | |
val wallet = if (i < req1.tx.getInputs().size()) wallet1!! else wallet2!! | |
val txIn = tx.getInput(i) | |
if (txIn.getConnectedOutput() == null) { | |
// Missing connected output, assuming already signed. | |
continue | |
} | |
val scriptPubKey = txIn.getConnectedOutput()!!.getScriptPubKey() | |
val redeemData = txIn.getConnectedRedeemData(wallet)!! | |
txIn.setScriptSig(scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript)) | |
} | |
val proposal = TransactionSigner.ProposedTransaction(tx) | |
LenientTransactionSigner().signInputs(proposal, wallet1!!) | |
LenientTransactionSigner().signInputs(proposal, wallet2!!) | |
tx.verify() | |
wallet1!!.commitTx(tx) | |
wallet2!!.commitTx(tx) | |
println(Utils.HEX.encode(tx.bitcoinSerialize())) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment