User Transactions
User transactions are created by any users interacting with the network. For this example, we'll show you an interesting way of verifying and validating a user's email address in a decentralized fashion.
Generating
Since these transactions are meant to be generated by users, they will resemble a CLI command or frontend form submission. In this example, we will use the vorpal
npm
package to create the initial transaction data using a CLI command.
async function injectTx(tx) {
try {
const res = await axios.post(`http://${HOST}/inject`, tx)
return res.data
} catch (err) {
return err.message
}
}
vorpal.command('email', 'registers your email address to the network').action(async function(_, callback) {
const answer = await this.prompt({
type: 'input',
name: 'email',
message: 'Enter your email address: ',
validate: result => {
const regex = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
if (!regex.test(result)) { // Check to make sure email is valid
return 'You need to provide a valid email address'
} else {
return true
}
},
})
const signedTx = {
emailHash: crypto.hash(answer.email), // Send a signed tx with the hash of the email so nobody can publicly see it
from: USER.address,
}
crypto.signObj(signedTx, USER.keys.secretKey, USER.keys.publicKey)
const tx = {
type: 'email', // the type of transaction being submitted
signedTx, // The signed transaction data
email: answer.email, // The answered form email
timestamp: Date.now(), // timestamp of the transaction
}
injectTx(tx).then(res => { // Submit transaction to the network
this.log(res)
callback()
})
})
Keys
The keys of any transaction are going to resemble the public keys of the accounts associated with the transaction. We are required to specify these in crack(). The only key involved with this transaction is the signed from
address (the public key of the user who submitted it).
case 'email':
result.sourceKeys = [tx.signedTx.from]
break
Validation
First, we will quickly validate the types and attributes of the transaction data using validate().
case 'email': {
if (typeof tx.signedTx !== 'object') {
success = false
reason = '"signedTx" must be an object.'
throw new Error(reason)
}
const signedTx = tx.signedTx
if (signedTx) {
if (typeof signedTx !== 'object') {
success = false
reason = '"signedTx" must be a object.'
throw new Error(reason)
}
if (typeof signedTx.sign !== 'object') {
success = false
reason = '"sign" property on signedTx must be an object.'
throw new Error(reason)
}
if (typeof signedTx.from !== 'string') {
success = false
reason = '"From" must be a string.'
throw new Error(reason)
}
if (typeof signedTx.emailHash !== 'string') {
success = false
reason = '"emailHash" must be a string.'
throw new Error(reason)
}
}
if (typeof tx.email !== 'string') {
success = false
reason = '"email" must be a string.'
throw new Error(reason)
}
if (tx.email.length > 30) {
success = false
reason = '"Email" length must be less than 31 characters (30 max)'
throw new Error(reason)
}
break
}
Next, we validate the transaction using actual account data using the helper function validateTransaction
.
const response: Shardus.IncomingTransactionResult = {
success: false,
reason: 'Transaction is not valid.',
txnTimestamp: tx.timestamp,
}
const from = wrappedStates[tx.from] && wrappedStates[tx.from].data
case 'email': {
const source: UserAccount = wrappedStates[tx.signedTx.from] && wrappedStates[tx.signedTx.from].data
if (!source) {
response.reason = 'no account associated with address in signed tx'
return response
}
if (tx.signedTx.sign.owner !== tx.signedTx.from) {
response.reason = 'not signed by from account'
return response
}
if (crypto.verifyObj(tx.signedTx) === false) {
response.reason = 'incorrect signing'
return response
}
if (tx.signedTx.emailHash !== crypto.hash(tx.email)) {
response.reason = 'Hash of the email does not match the signed email hash'
return response
}
response.success = true
response.reason = 'This transaction is valid!'
return response
}
Applying
The way this transaction works is that it selectively chooses one random node whose ID is closest to the address of the user who initiated the transaction. This "lucky" node can then make a single request to an email server API that will send a confirmation email to the user with a random 6-digit verification number. The tricky part is that only this node knows what the random number is, so we have to implement another transaction type that should be immediately injected during the apply phase of the email
transaction. This new transaction will be gossiped to every node, and they will be given the hash of the original verification number, so no user could see the original. Later, you could implement a verify
transaction that requires the user to send the verification number to the network and change the state of that user's account data to reflect the validation in some way.
case 'email': {
const source: UserAccount = wrappedStates[tx.signedTx.from].data
const nodeId = dapp.getNodeId()
const { address } = dapp.getNode(nodeId)
const [closest] = dapp.getClosestNodes(tx.signedTx.from, 5)
if (nodeId === closest) {
const baseNumber = 99999
const randomNumber = Math.floor(Math.random() * 899999) + 1
const verificationNumber = baseNumber + randomNumber
axios.post('http://somehost.com/mailAPI/index.cgi', {
from: 'liberdus.verify',
to: `${tx.email}`,
subject: 'Verify your email for liberdus',
message: `Please verify your email address by sending a "verify" transaction with the number: ${verificationNumber}`,
secret: 'some-secret-code',
})
dapp.put({
type: 'gossip_email_hash',
nodeId,
account: source.id,
from: address,
emailHash: tx.signedTx.emailHash,
verified: crypto.hash(`${verificationNumber}`),
timestamp: Date.now(),
})
}
dapp.log('Applied email tx', source)
break
}