Implementing bamboo, using only browser compatible cryptography.
Ailuropoda is the science name for a panda.
npm i -S @bicycle-codes/ailuropoda
Import types and functions.
import {
create as createMsg,
SignedPost,
lipmaaLink,
createBatch,
getLipmaaPath,
isValid,
verifyLipmaas
} from '@bicycle-codes/ailuropoda'
Log entries are { metadata, content }
, where metadata is
a signed object like below.
interface Metadata {
timestamp:number;
proof:string,
key:string, // <-- base64url encoded
seq:number;
lipmaalink:string|null;
prev:string|null;
username:string;
author:DID;
}
import { SignedMessage } from '@bicycle-codes/message'
type SignedMetadata = SignedMessage<Metadata>
export interface Content {
text:string,
alt?:string[],
mentions?:string[]
}
type SignedPost = { metadata:SignedMetadata, content:Content }
Use the function createBatch
to create a list with lipmaa links.
See the diagram for a nice visualization of the list structure.
import { Identity, create as createID } from '@bicycle-codes/identity'
import { createCryptoComponent } from '@ssc-half-light/node-components'
import { createBatch } from '@bicycle-codes/ailuropoda'
const alicesCrytpo = await createCryptoComponent()
const alice = await createID(alicesCrytpo, {
humanName: 'alice',
humanReadableDeviceName: 'computer'
})
const newMsgs = [
{ content: { text: 'hello 1' } },
{ content: { text: 'hello 2' } },
{ content: { text: 'hello 3' } },
{ content: { text: 'hello 4' } },
{ content: { text: 'hello 5' } }
]
const list = await createBatch(alice, alicesCrytpo, {
// we are just using an in-memory array of messages
getKeyFromIndex: async (i:number, msgs:SignedPost[]) => {
const msg = msgs[i]
if (!msg) return null
return msg.metadata.key
}
}, newMsgs) // pass in a list with message content
Get the lipmaa number number given a sequence number.
function lipmaaLink (n:number):number
const lipmaas = ([...Array(41).keys()]).map(n => {
return { lipmaa: lipmaaLink(n), n }
})
lipmaas
is like this:
[
{ lipmaa: 0, n: 0 }, { lipmaa: 0, n: 1 },
{ lipmaa: 1, n: 2 }, { lipmaa: 2, n: 3 },
{ lipmaa: 1, n: 4 }, { lipmaa: 4, n: 5 },
{ lipmaa: 5, n: 6 }, { lipmaa: 6, n: 7 },
{ lipmaa: 4, n: 8 }, { lipmaa: 8, n: 9 },
{ lipmaa: 9, n: 10 }, { lipmaa: 10, n: 11 },
{ lipmaa: 8, n: 12 }, { lipmaa: 4, n: 13 },
{ lipmaa: 13, n: 14 }, { lipmaa: 14, n: 15 },
{ lipmaa: 15, n: 16 }, { lipmaa: 13, n: 17 },
{ lipmaa: 17, n: 18 }, { lipmaa: 18, n: 19 },
{ lipmaa: 19, n: 20 }, { lipmaa: 17, n: 21 },
{ lipmaa: 21, n: 22 }, { lipmaa: 22, n: 23 },
{ lipmaa: 23, n: 24 }, { lipmaa: 21, n: 25 },
{ lipmaa: 13, n: 26 }, { lipmaa: 26, n: 27 },
{ lipmaa: 27, n: 28 }, { lipmaa: 28, n: 29 },
{ lipmaa: 26, n: 30 }, { lipmaa: 30, n: 31 },
{ lipmaa: 31, n: 32 }, { lipmaa: 32, n: 33 },
{ lipmaa: 30, n: 34 }, { lipmaa: 34, n: 35 },
{ lipmaa: 35, n: 36 }, { lipmaa: 36, n: 37 },
{ lipmaa: 34, n: 38 }, { lipmaa: 26, n: 39 },
{ lipmaa: 13, n: 40 }
]
Note the lipmaa
vs n
properties match with this diagram.
Create a message. This does not deal with lipmaa links. You would need to pass them in.
async function create (
user:Identity,
crypto:Implementation,
opts:{
content:Content,
limpaalink?:string|null, // <-- the key of the lipmaa message
seq:number,
prev:SignedPost|null|undefined,
}
):Promise<SignedPost>
import { create as createMsg } from '@bicycle-codes/ailuropoda'
const post = await createMsg(
alice,
alicesCrytpo,
{
seq: 1,
prev: null,
content: {
text: 'hello'
}
}
)
Verify a message. This does not look at links, only the signature and hash.
async function isValid (msg:SignedPost):Promise<boolean>
const { isOk } = await isValid(post)
// => true
Check that all the messages between the given message and message number 1 are valid. This will use the shortest path from the given message to the first message.
async function verifyLipmaas ({
messageFromKey
}:{
messageFromKey:(key:string)=>Promise<SignedPost>
}, msg:SignedPost, path?:number[]):Promise<{
isOk: boolean,
path:number[]
}>
const { isOk, path } = await verifyLipmaas(list2, {
messageFromKey
}, list2[39]) // array is 0 indexed, so 39 is seq number 40
// isOk = true
// path = [40, 13, 4, 1]
Get the shortest path between the given sequence number and
the first message. The parameter prev
is used internally, for recusion.
function getLipmaaPath (seq:number, prev?:number[]):number[]
Return an array of sequence numbers, starting with the first:
[ 1, 4, 13 ]
Create a linked list of the given messages, with lipmaa links.
async function createBatch (
user:Identity,
crypto:Implementation,
opts: {
getKeyFromIndex:(i:number, msgs:SignedPost[]) => Promise<string|null>
},
msgs:{
content:Content,
seq?:number,
prev?:SignedPost|null|undefined,
}[],
_out?:SignedPost[]
):Promise<SignedPost[]>
Create a linked list with in-memory content, starting from entry number 1.
Note in the example, getKey
is synchronous, but we need to return a
promise because that's what the API expects.
Takes a parameter getKeyFromIndex
that will return the key for an entry
given its index.
const newMsgs = [
{ content: { text: 'hello 1' } },
{ content: { text: 'hello 2' } },
{ content: { text: 'hello 3' } },
{ content: { text: 'hello 4' } },
{ content: { text: 'hello 5' } }
]
const list = await createBatch(alice, alicesCrytpo, {
getKeyFromIndex: getKey
}, newMsgs)
async function getKey (i:number, msgs:SignedPost[]):Promise<string|null> {
const msg = msgs[i]
if (!msg) return null
return msg.metadata.key
}
Given a previous message and a function that will return a message by its sequence number, create a new message with the correct lipmaa key.
async function append (
user:Identity,
crypto:Implementation,
opts:{
getBySeq:(seq:number) => Promise<SignedPost>
content:Content,
prev:SignedPost
}
):Promise<SignedPost>
const list = await createBatch(alice, alicesCrytpo, {
getKeyFromIndex: async (i, msgs) => {
return msgs[i].metadata.key
},
}, msgs)
const newMsg = await append(alice, alicesCrytpo, {
getBySeq: async (seq) => {
return list[seq - 1] // 0 vs 1 indexed
},
prev: list[list.length - 1],
content: { text: 'hello 40' }
})
Generated via typescript.