Register CRDT APIs

A topic for discussing the Autonomi Register CRDT API.

This post is a Wiki so please edit it to include useful links to documentation, demos, code etc.

Original Topic created by @happybeing but became broken

1 Like

From early hacking, it looks like registers arenā€™t append only atm (I thought they were). So, it looks like anyone with write access can merge the tree and delete the history.

That makes having a public writable register more like a variable that anyone can change. Unless Iā€™m missing something (quite possible at this stage).

EDIT:

Specifically, someone with write access can run client.get_register(address).write_merging_branches("replace all entries with this").

Register APIs need their own topic - shall we move your reply and this one to a topic for that ā€œRegister APIsā€? cc @moderators

[EDIT: @rob I think you are a mod here, please can you move the above reply by Paul and everything below to the topic: Register CRDT APIs (fixing this topic up, sorry Happybeing) Thanks]

Registers are append only but still very raw and it isnā€™t clear how they are supposed to be used. I have a pretty good idea of what the underlying RegisterCRDT can and canā€™t do, but I donā€™t think Autonomi want to leave that open and fully flexible.

What the RegisterCRDT doesnā€™t do as far as I can see is handle history in the way you might expect, so there is in effect one history per variable (as you put it) and the latest ā€˜valuesā€™ will be the ā€˜headsā€™ (of the RegisterCRDT). But these histories are then independent and cannot be stepped through in sync, unless you keep state externally to record that.

To get at those histories though, requires access to the underlying RegisterCRDT tree, which I exposed in a PR to allow the examples to print the structure of a register. But I donā€™t think Autonomi want that left exposed, so I expect them to extend the higher level Register APIs to provide the features they want exposed while hiding the underlying MerkleReg.

Until they get to that Iā€™m not sure if it will be a good idea or if best left open for developers. It could limit their usefulness but it may be better overall. :man_shrugging:

1 Like

Thanks - thatā€™s really useful context. Agree this needs its own thread. Apologies for just firing out thoughts!

Iā€™m in my mobile, but from poking around the code, I realised that client.get_register(name).read() returns the last entry and not the root.

EDIT: Looking again, the comment says it returns the last element, but looking at the code, I think it actually returns the whole tree. That means the following does remove the history:

client.get_register(address).write_merging_branches("replace all entries with this")

I tried calling register.merkle_reg().read().values (which you added as above, right?) to ensure I was seeing the full tree and it is the same after calling write_merging_branches (it has 1 entry).

Unless Iā€™m missing something?

1 Like

Iā€™m in a different space right now but from memory merging writes a new entry on top of one or more concurrent entries. They may call it ā€˜replacingā€™ because it replaces the head(s) but they remain in the tree, just out of sight. The tricky bit is they are now hidden unless you access the MerkelTree. I noticed that Gab did expose a little more recently but havenā€™t looked at that.

You might like to look at how heā€™s using Registers (in the Folders API). From memory each directory is a register and heā€™s maintaining a separate history for each directory history (in a single RegisterCRDT per directory). Each file entry points to file metadata. Each directory entry points to the register for that subdirectory.

This means that the ā€˜historyā€™ of each entry in a directory is separate though, and you canā€™t step through the history of the directory itself because the MerkleTree isnā€™t structured to do that. You would need to store state externally, if that would work IDK.

I came up with a design for doing this differently, but while that could store the whole history of a directory tree it had its own downsides. Iā€™m not sure how useful history really is without that though. Itā€™s a debate to be had, but probably best with examples to play with. I gave up on that area and switched to something else where there will be debate - hence my posts about web and NRS on this forum. But it will be better to have when there is something to play with.

1 Like

FWIW, from messing about with registers on my local test net:

println!("Adding entries for set1:");

let hash1 = register.write_merging_branches("entry1,val1".as_bytes()).expect("failed to add child");

for (entry) in register.merkle_reg().read().values() {
    let entry_str = String::from_utf8(entry.clone()).unwrap_or_else(|_| format!("{entry:?}"));
    println!("output set1: {entry_str}");
}

println!("write_atop new entries for set2:");

register.write_atop("entry2,val2".as_bytes(), &vec![hash1].into_iter().collect()).expect("failed to add child");
register.write_atop("entry3,val3".as_bytes(), &vec![hash1].into_iter().collect()).expect("failed to add child");
register.write_atop("entry4,val4".as_bytes(), &vec![hash1].into_iter().collect()).expect("failed to add child");
register.write_atop("entry5,val5".as_bytes(), &vec![hash1].into_iter().collect()).expect("failed to add child");

for (entry) in register.merkle_reg().read().values() {
    let entry_str = String::from_utf8(entry.clone()).unwrap_or_else(|_| format!("{entry:?}"));
    println!("output set2: {entry_str}");
}

println!("merging branches for set3");
register.write_merging_branches("entry6,val6".as_bytes()).expect("failed to replace register!");

for (entry) in register.merkle_reg().read().values() {
    let entry_str = String::from_utf8(entry.clone()).unwrap_or_else(|_| format!("{entry:?}"));
    println!("output set3: {entry_str}");
}

let entries = register.read();
println!("Register entries:");

// print all entries
for (entry) in entries.clone() {
    let (hash, bytes) = entry.clone();
    let data_str = String::from_utf8(bytes.clone()).unwrap_or_else(|_| format!("{bytes:?}"));
    println!("Entry - hash {hash}, data: {data_str}");
}

Outputs:

Adding entries for set1:
output set1: entry1,val1
write_atop new entries for set2:
output set2: entry4,val4
output set2: entry5,val5
output set2: entry2,val2
output set2: entry3,val3
merging branches for set3
output set3: entry6,val6
Register entries:
Entry - hash 373a8d.., data: entry6,val6

So, it looks like the ā€˜historyā€™ is being removed due to branch merge. Again, I could be doing something wrong, but this feels like full write, rather than append only.

1 Like

I wonā€™t be able to look at this for a while, but if you havenā€™t looked at the example in the SN readme which prints the structure it might help.

Iā€™m not saying what you are doing isnā€™t destroying the history. If you are then thatā€™s a kind of misuse. Merge itself shouldnā€™t do that. I can imagine that using the higher level APIs it looks like it though, because they donā€™t give access to the history, which is why I exposed the low level register.

Keep poking!

Off now, bacon calls! :sandwich: