Transactions, transforms, proofs

Transaction

The Transaction is an Applicable, Marshallable, and Hashable object. Transactions are the units of work used to manipulate state on the blockchain. Transactions consist of always one Transform to mutate state and one-or-many Proofs to authenticate the changes proposed in the Transform.

To construct a Transaction its dependencies should be constructed and injected. First, a Transform must be constructed, each transform’s constructor will differ based on its behaviour. The transforms are what will essentially define the business logic of the application.

Once the Transform has been created, at least one Proof needs to be added to the transaction. Use Transform.hash() method to generate a challenge for the Proof, and Proof.sign(key) method to sign the transaction. Note that a Transform has a required_authorizations method which will return a list of mandatory addresses that must provide a Proof.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 from credits.transform import BalanceTransform
 from credits.proof import SingleKeyProof
 from credits.key import ED25519SigningKey
 from credits.address import CreditsAddress
 from credits.hash import SHA256HashProvider

 HASH_PROVIDER = SHA256HashProvider()

 ALICE_SK = ED25519SigningKey.new()
 ALICE_VK = ALICE_SK.get_verifying_key()
 ALICE_ADDR = CreditsAddress(ALICE_VK.to_string()).get_address()

 BOB_SK = ED25519SigningKey.new()
 BOB_VK = BOB_SK.get_verifying_key()
 BOB_ADDR = CreditsAddress(BOB_VK.to_string()).get_address()

 transform = BalanceTransform(ALICE_ADDR, BOB_ADDR, 100)
 # transform.required_authorizations() ==  [ALICE_ADDR]

 transaction = Transaction(
     transform=transform,
     proofs=[
         SingleKeyProof(ALICE_ADDR, 0, transform.hash(HASH_PROVIDER)).sign(ALICE),
     ]
 )

 payload = transaction.marshall()  # This output can be jsonified and POSTed to /api/v1/transaction

Transform

Transform is an Applicable, Marshallable and Hashable object. Transforms are constructed with all necessary values required to modify state during the apply() method call and are used as a standard unit of work inside the Credits Framework to modify Blockchain State.

When a Transaction with Transform inside is onboarded to the Node, it is verified against the current Blockchain State. The verify() method should perform checks against the state to check if this Transform will be applicable either now or sometime in the future. If a Transform fails verification at any point it will be flushed from the Node. Note that if a Transform attempts to modify state during its validation, changes made to the state will be disposed of.

When a Transform is applied it will be given the current state of the Network and expected to modify and return a new state. During the transaction application, a Transform may perform any verification that has to be performed “upon application”. If this verification fails, the apply should fail and return an erroneous result. However, failure of apply doesn’t cause the transaction to be discarded. It stays in the unconfirmed pool until it’s either gets confirmed or it’s verify method also fails. Only when verify fails transaction is discarded and forgotten.

Hash storage transform

Hash storage use case is probably the simplest one possible on the blockchain. In this case following transform can be used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 class LogHashTransform(Transform):
     STATE_BALANCE = "core.credits.log.hashes"

     def __init__(self, hash):
         self.hash = hash

     def verify(self, state):
         if state[self.LOG_STATE][self.hash]:
             return None, "Already have this hash logged!"
         return None, None

     def apply(self, state):
         state[self.LOG_STATE][self.hash] = {"logged_at": time.asctime()}
         return state, None

This transform will first verify the hash is not already loaded. If it is loaded then it fails. When it comes to the application then it simply sets the hash against the time it was applied to the state of the world. Taking this idea a more complex KYC or logging system could easily be developed.

Balance transfer transform

Here is an example implementation of a simple balance transfer transform. It implements credits.transform.Transform interface and required sanity checks for transferring basic token balances between accounts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 from credits import transform
 from credits import stringify
 from credits import test

 """
 In this example we create a basic "Balance Transfer" transform. We then use
 the credits.test.check_transform() to validate that all expected
 attributes/methods/behaviours are provided.
 """

 class BalanceTransform(transform.Transform):
     fqdn = "credits.test.BalanceTransform"

     def __init__(self, addr_from, addr_to, amount):
         self.addr_from = addr_from
         self.addr_to = addr_to
         self.amount = amount

     def marshall(self):
         return {
             "fqdn": self.fqdn,
             "addr_to": self.addr_to,
             "addr_from": self.addr_from,
             "amount": self.amount,
         }

     @classmethod
     def unmarshall(cls, registry, payload):
         return cls(
             addr_from=payload["addr_from"],
             addr_to=payload["addr_to"],
             amount=payload["amount"],
         )

     def verify(self, state):
         """
         Verify it is possible, to apply either now or in future. Return an
         errornous response if verification fals.
         """
         balances = state["credits.test.Balances"]

         if self.addr_from not in balances:
             return None, "%s not in credits.test.Balances."

         if self.amount <= 0:
             return None, "amount must be greater than 0."

         if balances[self.addr_from] < self.amount:
             return None, "%s does not have balance to make transfer."

         return None, None  # valid transaction

     def apply(self, state):
         try:
             balances = state["credits.test.Balances"]
             balances[self.addr_from] -= self.amount
             balances[self.addr_to] = balances.get(self.addr_to, 0) + self.amount  # addr_to might not exist.
             return state, None  # return the new state.

         except Exception as e:
             return None, e.args[0]  # Something went really wrong, don't apply.

     def hash(self, hash_provider):
         return hash_provider.hexdigest(stringify.stringify(self.marshall()))

You can find this example in balance_transform.py.

In this example verify method references to now or in future, this is because of the way the verify and apply logic is working in conjunction with the unconfirmed transactions pool. The verify is called against current global state once the transaction is trying to be onboarded, and if it passes (i.e. doesn’t return an error) - the transaction is onboarded into node’s unconfirmed transaction pool. However at this point, the apply is not yet invoked. Once the node will attempt to put the transaction into a block it will call the transform’s apply method, and that method may have it’s own additional verification logic. For example, the simplest case can be the proof’s nonce check. In the transaction’s verify method the nonce expected to be equal or greater than the current nonce recorded in the global state. That mean the nonce can be just next one in line, or far greater than the one in global state.

However, the apply logic by default requires transaction nonce to be exactly equal to global state, so the transaction with nonce far off will fail to apply. In this situation, the transaction that will successfully verify but will fail to apply will hang in the unconfirmed transactions pool until the time for it will come, or until verify itself will fail and the transaction will be discarded.

Understanding this nuanced mechanics allows creating customised behaviours with complex future dependencies and delayed execution.

Proof

The Proof is an Applicable object requiring both verify and apply methods implemented. Proofs are constructed with some sort of resolvable address, a nonce (which is typically an autoincrementing number), and a challenge to sign. This challenge will typically be the hash of a Transform.

Once constructed a Proof is unsigned and a sign method must be called with a signing_key to generate a verifying_key and signature. Once signed a Proof is now considered valid as during it’s verify call it will attempt to convert the verifying_key into an address. This address will be compared to the address the Proof was constructed with.

When Proofs are onboarded to the Node as a part of Transaction, they are verified against State to check that a Signature exists as well as any proof specific ordering is valid. If a Proof is onboarded in an unsigned state it’s parent Transaction will be discarded.

Note: This is not a complete Proof example, it has been reduced to show just the verify, apply, and sign logic. If you need a working example you should import credits.proof.SingleKeyProof from the Common Library and use that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 from credits.proof import Proof
 from credits.address import CreditsAddressProvider


 class SingleKeyProof(Proof):
     fqdn = 'works.credits.core.SingleKeyProof'
     STATE_NONCE = "works.credits.core.IntegerNonce"

     def __init__(self, address, nonce, challenge, verifying_key=None, signature=None):
         super(SingleKeyProof, self).__init__()

         self.address = address
         self.nonce = nonce
         self.challenge = challenge
         self.verifying_key = verifying_key
         self.signature = signature

     def verify(self, state):
         """
         Verify this proof has been signed and that it's
         signature/verifying_key/challenge is valid against state.

         :returns: result, error
         """
         if (self.signature is None) or (self.verifying_key is None):
             error = "Proof has not been signed."
             self.logger.error(error)
             return None, error

         # Generate an address for this verifying_key, we'll need to validate
         # the key used to sign this proof resolves to a predetermined address.
         nonces = state[self.STATE_NONCE]
         address = CreditsAddressProvider(self.verifying_key.to_string()).get_address()

         if address != self.address:
             error = "Proof for address {} was signed with {}".format(self.address, address)
             self.logger.error(error)
             return None, error

         if not self.verifying_key.verify(self.challenge, self.signature):
             error = "SingleKeyProof failed a signature check against {}".format(address)
             self.logger.error(error)
             return None, error

         known_nonce = nonces[address]
         if self.nonce < known_nonce:
             error = "SingleKeyProof nonce ({}) is less than current nonce ({}) for {}".format(
                 self.nonce,
                 known_nonce,
                 address
             )
             self.logger.error(error)
             return None, error

         return state, None

     def apply(self, state):
         """
         Apply this proof by incrementing the target address' nonce forwards.
         This stops this Proof's parent Transaction from being executed.
         """
         nonces = state[self.STATE_NONCE]
         address = CreditsAddressProvider(self.verifying_key.to_string()).get_address()

         if self.nonce != nonces[address]:
             error = "SingleKeyProof nonce ({}) is not equal to nonce ({}) for {}".format(
                 self.nonce,
                 nonces[address],
                 address
             )
             self.logger.error(error)
             return None, error

         nonces[address] += 1

         return state, None

     def sign(self, signing_key):
         """
         Sign this proof.

         :type signing_key: credits.key.SigningKey
         :rtype: credits.proof.SingleKeyProof
         """
         verifying_key = signing_key.get_verifying_key()
         signature = signing_key.sign(self.challenge)

         return SingleKeyProof(
             address=self.address,
             nonce=self.nonce,
             challenge=self.challenge,
             verifying_key=verifying_key,
             signature=signature,
         )