mirror of
https://github.com/corda/corda.git
synced 2025-04-06 19:07:08 +00:00
ENT-2168: Add a shell command to check for an existing transaction (#3762)
* ENT-2168: Add a shell command to check for an existing transaction When a double-spend occurs the notary returns the hash of the consuming transaction id. I've added a 'hash-lookup' shell command that matches any recorded transactions on the node against this id hash to determine whether the state has been consumed by this node (that could happen in certain race conditions).
This commit is contained in:
parent
66739c138c
commit
ce5f38104b
@ -27,9 +27,9 @@ sealed class NotaryError {
|
||||
/** Specifies which states have already been consumed in another transaction. */
|
||||
val consumedStates: Map<StateRef, StateConsumptionDetails>
|
||||
) : NotaryError() {
|
||||
override fun toString() = "Conflict notarising transaction $txId. " +
|
||||
"Input states have been used in another transactions, count: ${consumedStates.size}, " +
|
||||
"content: ${consumedStates.asSequence().joinToString(limit = 5) { it.key.toString() + "->" + it.value }}"
|
||||
override fun toString() = "One or more input states have already been used in other transactions. Conflicting state count: ${consumedStates.size}, consumption details:\n" +
|
||||
"${consumedStates.asSequence().joinToString(",\n", limit = 5) { it.key.toString() + " -> " + it.value }}.\n" +
|
||||
"To find out if any of the conflicting transactions have been generated by this node you can use the hashLookup Corda shell command."
|
||||
}
|
||||
|
||||
/** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */
|
||||
|
@ -0,0 +1,96 @@
|
||||
package net.corda.tools.shell
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.io.Files
|
||||
import com.jcraft.jsch.ChannelExec
|
||||
import com.jcraft.jsch.JSch
|
||||
import com.jcraft.jsch.Session
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import org.bouncycastle.util.io.Streams
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class HashLookupCommandTest {
|
||||
@Ignore
|
||||
@Test
|
||||
fun `hash lookup command returns correct response`() {
|
||||
val user = User("u", "p", setOf(Permissions.all()))
|
||||
|
||||
driver(DriverParameters(notarySpecs = emptyList(), extraCordappPackagesToScan = listOf("net.corda.testing.contracts"))) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
val txId = issueTransaction(node)
|
||||
|
||||
val session = connectToShell(user, node)
|
||||
|
||||
testCommand(session, command = "hashLookup ${txId.sha256()}", expected = "Found a matching transaction with Id: $txId")
|
||||
testCommand(session, command = "hashLookup ${SecureHash.randomSHA256()}", expected = "No matching transaction found")
|
||||
|
||||
session.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun testCommand(session: Session, command: String, expected: String) {
|
||||
val channel = session.openChannel("exec") as ChannelExec
|
||||
channel.setCommand(command)
|
||||
channel.connect(5000)
|
||||
|
||||
assertTrue(channel.isConnected)
|
||||
|
||||
val response = String(Streams.readAll(channel.inputStream))
|
||||
val matchFound = response.lines().any { line ->
|
||||
line.contains(expected)
|
||||
}
|
||||
channel.disconnect()
|
||||
assertTrue(matchFound)
|
||||
}
|
||||
|
||||
private fun connectToShell(user: User, node: NodeHandle): Session {
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
sshdPort = 2224)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
InteractiveShell.nodeInfo()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2224)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("p")
|
||||
session.connect()
|
||||
|
||||
assertTrue(session.isConnected)
|
||||
return session
|
||||
}
|
||||
|
||||
private fun issueTransaction(node: NodeHandle): SecureHash {
|
||||
return node.rpc.startFlow(::DummyIssue).returnValue.get()
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
internal class DummyIssue : FlowLogic<SecureHash>() {
|
||||
@Suspendable
|
||||
override fun call(): SecureHash {
|
||||
val me = serviceHub.myInfo.legalIdentities.first().ref(0)
|
||||
val fakeNotary = me.party
|
||||
val builder = DummyContract.generateInitial(1, fakeNotary as Party, me)
|
||||
val stx = serviceHub.signInitialTransaction(builder)
|
||||
serviceHub.recordTransactions(stx)
|
||||
return stx.id
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package net.corda.tools.shell;
|
||||
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping;
|
||||
import org.crsh.cli.Argument;
|
||||
import org.crsh.cli.Command;
|
||||
import org.crsh.cli.Man;
|
||||
import org.crsh.cli.Usage;
|
||||
import org.crsh.text.Color;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HashLookupShellCommand extends InteractiveShellCommand {
|
||||
private static Logger logger = LoggerFactory.getLogger(HashLookupShellCommand.class);
|
||||
|
||||
@Command
|
||||
@Man("Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" +
|
||||
"This is mainly intended to be used for troubleshooting notarisation issues when a\n" +
|
||||
"state is claimed to be already consumed by another transaction.\n\n" +
|
||||
"Example usage: hash-lookup E470FD8A6350A74217B0A99EA5FB71F091C84C64AD0DE0E72ECC10421D03AAC9"
|
||||
)
|
||||
public void main(@Usage("A hexadecimal SHA-256 hash value representing the hashed transaction Id") @Argument(unquote = false) String txIdHash) {
|
||||
logger.info("Executing command \"hash-lookup\".");
|
||||
|
||||
if (txIdHash == null) {
|
||||
out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Color.red);
|
||||
return;
|
||||
}
|
||||
|
||||
CordaRPCOps proxy = ops();
|
||||
List<StateMachineTransactionMapping> mapping = proxy.stateMachineRecordedTransactionMappingSnapshot();
|
||||
|
||||
SecureHash txIdHashParsed;
|
||||
try {
|
||||
txIdHashParsed = SecureHash.parse(txIdHash);
|
||||
} catch (IllegalArgumentException e) {
|
||||
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Color.red);
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<SecureHash> match = mapping.stream()
|
||||
.map(StateMachineTransactionMapping::getTransactionId)
|
||||
.filter(
|
||||
txId -> SecureHash.sha256(txId.getBytes()).equals(txIdHashParsed)
|
||||
)
|
||||
.findFirst();
|
||||
|
||||
if (match.isPresent()) {
|
||||
SecureHash found = match.get();
|
||||
out.println("Found a matching transaction with Id: " + found.toString());
|
||||
} else {
|
||||
out.println("No matching transaction found", Color.red);
|
||||
}
|
||||
}
|
||||
}
|
@ -132,6 +132,7 @@ object InteractiveShell {
|
||||
ExternalResolver.INSTANCE.addCommand("run", "Runs a method from the CordaRPCOps interface on the node.", RunShellCommand::class.java)
|
||||
ExternalResolver.INSTANCE.addCommand("flow", "Commands to work with flows. Flows are how you can change the ledger.", FlowShellCommand::class.java)
|
||||
ExternalResolver.INSTANCE.addCommand("start", "An alias for 'flow start'", StartShellCommand::class.java)
|
||||
ExternalResolver.INSTANCE.addCommand("hashLookup", "Checks if a transaction with matching Id hash exists.", HashLookupShellCommand::class.java)
|
||||
shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user