3.4 Records

Usage

Until now, all the programs we have been writing did not have any state. This means there was no continuity between calls of the different transitions of our programs. We're going to need a mechanism to store and update some state if we want to create useful applications. Although we also want this state to have privacy built in. This is where records come into action.

As structs, records are data structures that are composed of keys and values. They are defined at the root of the program block:

program records.aleo {
    // Records can include arbitrary attribute types, even struct or arrays
    record Counter {
        owner: address,
        amount: u64
    }
    // ...

The difference with structs is that records MUST contain an owner address attribute. When a record is created, it is encrypted with the owner's public key and saved in a list of existing record on chain.

Here is an example of a transition that creates and outputs a record matching the previous definition.

    // ...
    transition init() -> Counter {
        let counter: Counter = Counter {
             // The output owner is the following address
            owner: aleo1hn9z4zt6awd6ts9zn8ppkwwvjwrg04ax48krd0fgnnzu6ezg9qxst3v26j,
            // Amount is initialized to 0
            amount: 0u64
        };
        return counter;
    }

Let's try to execute our init function using the CLI:

leo execute init

It should output something like the following:

⛓  Constraints

 •  'hello_world.aleo/init' - 2,020 constraints (called 1 time)

➡️  Output

 • {
  owner: aleo1hn9z4zt6awd6ts9zn8ppkwwvjwrg04ax48krd0fgnnzu6ezg9qxst3v26j.private,
  amount: 0u64.private,
  _nonce: 2339228690106642281685542089493361891235548124173473162277105528248154231682group.public
}

{
...
  "program":"records.aleo",
  "function":"init",
  "inputs":[],
  "outputs":[
    {
      "type":"record",
      "id":"8356863858197881954363187596447154000147063956529378891968365494207484956531field",
      "checksum":"1793050240459007306720215649548316823005636259029202267200792782971600003930field",
      "value":"record1qyqspk9djwf3fex3e3fdkam5c0qx8vjhccefcqx3x094q8znzc3tzpcsqyrxzmt0w4h8ggcqqgqsqvj9ghgthqq2mneg72d93xm7a9z3sghyfsyyj6s06evza8huuvc8s2fhgvjrk008sstc5gns23anzf044dqjmp3z7jqm32ah8hh59vzst9wyaq"
    }
  ]
...

As you can see in the execution at the bottom, the record output type is not private nor public, it is record. Its value, as for private values, is encrypted: record1qyqspk9djwf3fex3e3fdkam5c0qx8vjhccefcqx3x094q8znzc3tzpcsqyrxzmt0w4h8ggcqqgqsqvj9ghgthqq2mneg72d93xm7a9z3sghyfsyyj6s06evza8huuvc8s2fhgvjrk008sstc5gns23anzf044dqjmp3z7jqm32ah8hh59vzst9wyaq. But the difference with private type is it's not encrypted with the signer's public key, it's encrypted with the owner's public key. This means that once this transaction is on chain, the owner will be able to decrypt this record's data with his view key.

It can be decrypted using the snarkos CLI as follows:

snarkos developer decrypt \
  --ciphertext record1qyqspk9djwf3fex3e3fdkam5c0qx8vjhccefcqx3x094q8znzc3tzpcsqyrxzmt0w4h8ggcqqgqsqvj9ghgthqq2mneg72d93xm7a9z3sghyfsyyj6s06evza8huuvc8s2fhgvjrk008sstc5gns23anzf044dqjmp3z7jqm32ah8hh59vzst9wyaq \
  --view-key AViewKey1oLkT7wz583Gd7FCeuFB7WYDHm8QWoEfmNEaKHV3L4crP

Which outputs:

{
  owner: aleo1hn9z4zt6awd6ts9zn8ppkwwvjwrg04ax48krd0fgnnzu6ezg9qxst3v26j.private,
  amount: 0u64.private,
  _nonce: 2339228690106642281685542089493361891235548124173473162277105528248154231682group.public
}

You can see there is an extra _nonce attribute. It plays a role in how records encryption/decryption works, see corresponding section below.

Every record created in a previous transition can be then consumed once and ONLY once by its owner. To see how this works, let's add a function that consumes an existing records:

    // ...
    transition consume(counter: Counter) {}
    // ...

As you can see:

leo execute consume "{
  owner: aleo1hn9z4zt6awd6ts9zn8ppkwwvjwrg04ax48krd0fgnnzu6ezg9qxst3v26j.private,
  amount: 0u64.private,
  _nonce: 2339228690106642281685542089493361891235548124173473162277105528248154231682group.public
}"

Will return an error if not executed by aleo1hn9z4zt6awd6ts9zn8ppkwwvjwrg04ax48krd0fgnnzu6ezg9qxst3v26j:

Leo ✅ Compiled 'records.aleo' into Aleo instructions
Error [EPAK0375051]: ❌ Execution error: Input record for 'records.aleo' must belong to the signer

You cannot update an existing record. You have to consume it and create a new one. Let's add such a function that increments a previous counter. Here's the full code:

program records.aleo {
    record Counter {
        owner: address,
        amount: u64
    }

    transition init() -> Counter {
        let counter: Counter = Counter {
            owner: self.signer, // Owner was updated to signer (who executes the transition)
            amount: 0u64 // Amount is initiated to 0
        };
        return counter;
    }

    transition consume(counter: Counter) {}

    transition increment(old_counter: Counter) -> Counter {
        let new_counter: Counter = Counter {
            owner: old_counter.owner,
            amount: old_counter.amount + 1u64
        };
        return new_counter;
    }
}

As you can see, accessing record properties works the same way as for struct:

let amount: u64 = user.amount;

Here we are, we now have an encrypted state. Here it gets increased every time we call the increment function. This is a very basic example, but records are a powerful concept that can capture arbitrary logic for your applications.

Records are completely private. Not only does the data in records is encrypted, but it's impossible to know which address owns a record. It's also impossible to trace which transition created a record that is consumed in another transition.

How records work under the hood?

Encryption

A record's data is encoded as a list of field elements which are each encrypted with owner's view key. We'll explain here how a single data field dd is encrypted, which can then be repeated for each encoded fields.

Owner's view key is a scalar vv (derived from his private key, as detailed in chapter 1.4), and owner address is P=vGP=v\cdot G (where GG is the generator of the sub-group of elliptic curve points that generates group elements).

Here is the scheme to encrypt dd as someone who produces a record that will be owned by PP:

  • I generate a random scalar rr (picked uniformly among scalars).

  • I calculate the record nonce: N=rGN=r\cdot G.

  • I calculate the record view key: E=rPE=r\cdot P.

  • I hash record view key to get a field: h=hash(E)h=hash(E).

  • I can calculate record ciphertext: c=d+hc=d+h . is shared publicly on chain encoded in bech32 record1...

Decryption

Here is the decryption scheme, using PP's viewkey, vv:

  • PP calculates record view key: E=vNE=v\cdot N. That's because vN=v(rG)=r(vG)=rP=Ev\cdot N=v\cdot(r\cdot G)=r\cdot(v\cdot G)=r\cdot P=E.

  • PP hashes record view key to get a field: h=hash(E)h=hash(E).

  • PP can calculate plain text data d=hcd=h-c.

State Management

Records are identified on chain using a commitment to their data and stored in a Merkle tree. When consumed, a proof of inclusion of that record in the Merkle tree is included in the proof for the execution of the transition. Also a "nullifier", called the record serial number is published on the network to prevent future double spending of that record. This nullifier reveals no information about which record was consume, what data it includes, and who was the owner.

This is explained in detail in Zexe whitepaper: Zexe: Enabling Decentralized Private Computation, section 2.4.

Last updated