A living on-chain diary you grow as a plant. Write to water it; `neglect wilts it; every diary is` mortal. Fully on-chain, eternal`data:application/json;base64,A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``r{"name":"Seedling #`h - Revealing Soon","description"`3","attributes":[{"trait_type":"Status","value":"Revealing Soon"}` a ,V[o","attributes":[`{"trait_type":"Species","value":`{"trait_type":"Rarity","value":"`{"trait_type":"Status","value":"`{"trait_type":"Entries","value":`{"trait_type":"Watered Weeks","v`A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``,"diedAt":0,"entryCount":24,"wateredEpochs":20,"deathTime":a ,V[q,"revealed":false}`p<!doctype html><html><head><meta` <meta name="viewport" content="w`Qidth=device-width,initial-scale=`q<style>html,body{margin:0;height`:100%;background:transparent}</s`</script><script>SeedlingRendere)</script></body></html>RlGolden Wattle`RoBird of Paradise`RlChilean Guava`RmJapanese Maple`RsAntarctic Hair Grass``ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` data:image/svg+xml;base64,<svg xmlns="http://www.w3.org/20` 00/svg" viewBox="0 0 600 600"><rect width="600" height="600" f`^<rect x="30" y="30" width="540" `height="540" fill="none" stroke=`"#3a4a3a" stroke-width="2"/><text x="60" y="110" font-family`="Georgia,serif" font-size="40" a<text x="60" y="160" font-family`="Georgia,serif" font-size="22" `'<text x="60" y="520" font-family`="monospace" font-size="20" fill`'<text x="60" y="550" font-family`="monospace" font-size="20" fill`'="#5a6a5a">watered weeks a ,V[p,"revealed":true}`x
t templated, so nudge the base
// x by a seed-stable, bell-weighted amount (mostly small, occasional larger) — the whole plant +
// its companions shift together; the full-width ground line stays, so it just sits left/right of
// centre. Bounded so wide canopies don't clip. Species can opt out / override via spec.basePlace.
const baseDX = (spec.basePlace===false ? 0 : gauss(rng('placeX',st.seed)) * (spec.basePlace||45));
const env={ageF,m,density,structure:structureOf(spec,st),rho,rewards,reproOK,v,dead,sen,dormancy,
season:seasonOf(st.now),browny:clamp(Math.max(1-v,sen),0,1),now:st.now,W,H,baseX:BASEX+baseDX,baseY:BASEY};
env.pollinators=pollinatorsOf(spec,st,env);
return env;
}
function renderPlant(spec, st){
const env=computeEnv(spec,st);
const C=makeCanvas();
spec.draw(st,C,env);
let soil='';
if(!spec.noSoil){ // species can opt out of the drawn ground line
const soilC=mix((spec.PAL&&spec.PAL.soil)||'#6f5128','#9a9180',0.25);
for(let i=0;i<30;i++){ const x=40+i*((W-80)/29), yy=BASEY+16+Math.sin(i*1.7)*3;
soil+=glyph('.',x,yy,GLY,soilC,1.1); } }
const sc=schemeFor(st.seed);
const plant=C.resolve(sc, rng('struct',st.artSeed), spec.LW||DEFLW, spec.schemeExempt||['flowers'], spec.LS||DEFLS);
// ecosystem visitors: drawn ABOVE the plant in negative space; never the plant body (§6.3)
let poll='';
if(env.pollinators.length && !spec.placesPollinators){ // species may place its own instead
placePollinators(C, env.pollinators, st); // anchor to this species' actual blooms
for(const p of env.pollinators) poll+=drawPollinator(p,env);
}
const inner=soil+plant+(poll?`<g id="lyr-pollinators">${poll}</g>`:'');
return framed(BACKDROPS[backdropFor(st.seed)], inner);
}
// ---------- common stage / traits ----------
function defaultStage(spec, st, env){
if(st.diedAt>0) return 'Sealed';
const m=env.m, age=env.ageF;
if(age>0.85) return 'Senescent';
if(m<0.12) return 'Sprout'; if(m<0.4) return 'Young'; if(m<0.62) return 'Developing';
if(m<0.82) return 'Mature'; return 'Full';
}
function commonTraits(spec, st, env){
const stage = spec.stageName ? spec.stageName(st,env) : defaultStage(spec,st,env);
const rows = [
['Backdrop', BACKDROP_NAMES[backdropFor(st.seed)]],
['Scheme', SchemeName(schemeFor(st.seed))],
['Stage', stage],
['Vitality', Math.round(env.v*100)+'%'],
['Devotion', st.entryCount>=20?'High':st.entryCount>=8?'Medium':'Low'],
['Never wilted', st.everWilted?'No':'Yes'],
];
const ps=env.pollinators||[]; // earned via public love (>10 notes, in season)
// exact tokenURI attribute names — match ART_SPEC §8 / CONTRACT_SPEC §8.5 so the contract emits these
const cap=s=>s.charAt(0).toUpperCase()+s.slice(1);
rows.push(['Pollinator', ps.length ? [...new Set(ps.map(p=>p.type))].map(cap).join('/') : 'None']);
rows.push(['Pollinator Count', String(ps.length)]);
const morph=ps.map(p=>p.morph).find(Boolean); // rare collectible variant, if any
rows.push(['Pollinator Morph', morph?cap(morph):'None']);
return rows;
}
// ============================================================
// mount(spec): inject chrome + wire the controller
// ============================================================
function mount(spec){
document.title='Seedling · '+spec.title;
const accent=(spec.PAL&&spec.PAL.accent)||'#7FA66B';
document.body.innerHTML=`
<style>
:root{ --bg:#11140f; --panel:#1b1f17; --line:#2c3322; --ink:#e9e6d8; --muted:#9aa088; --accent:${accent}; }
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);
font:14px/1.5 ui-sans-serif,system-ui,sans-serif;display:flex;min-height:100vh}
.stage{flex:1;display:flex;align-items:center;justify-content:center;padding:22px}
.stage svg{height:min(90vh,880px);width:auto;box-shadow:0 20px 60px rgba(0,0,0,.45)}
.panel{width:340px;background:var(--panel);border-left:1px solid var(--line);padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:16px}
h1{font-size:15px;margin:0;letter-spacing:.05em;text-transform:uppercase}
h1 small{display:block;color:var(--muted);font-size:11px;letter-spacing:.12em;margin-top:4px}
.group{border:1px solid var(--line);border-radius:11px;padding:13px}
.group h2{font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--muted);margin:0 0 9px}
textarea{width:100%;min-height:52px;resize:vertical;background:#0e120b;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px;font:inherit}
.btns{display:flex;flex-wrap:wrap;gap:7px;margin-top:9px}
button{background:#23291b;color:var(--ink);border:1px solid var(--line);padding:8px 11px;border-radius:8px;cursor:pointer;font:inherit;transition:.15s}
button:hover{border-color:var(--accent);background:#2b3322}
button.primary{background:var(--accent);color:#10140c;border-color:var(--accent);font-weight:600}
button:disabled{opacity:.4;cursor:not-allowed}
.stat{display:flex;justify-content:space-between;align-items:center;margin:6px 0;font-size:13px}
.stat b{font-variant-numeric:tabular-nums}
.bar{height:7px;background:#0e120b;border-radius:5px;overflow:hidden;margin-top:3px}
.bar>i{display:block;height:100%;border-radius:5px;transition:width .35s}
.meta{display:grid;grid-template-columns:1fr 1fr;gap:5px 12px;font-size:12.5px}
.pill{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;background:#0e120b;border:1px solid var(--line);color:var(--muted)}
.pill.alive{color:#9fd68a;border-color:#3c5a30}.pill.dead{color:#d98a8a;border-color:#5a3030}
.traits{display:flex;flex-wrap:wrap;gap:6px}.traits .t{font-size:11px;padding:3px 8px;border-radius:6px;background:#0e120b;border:1px solid var(--line)}
.note{color:var(--muted);font-size:11px;line-height:1.5}
a.dl{color:var(--accent);font-size:12px;text-decoration:none}
</style>
<div class="stage"><div id="art"></div></div>
<div class="panel">
<h1>Seedling <small>${spec.title} · ${spec.sub||'v1'}</small></h1>
<div class="group"><h2>Write an entry (waters the plant)</h2>
<textarea id="entry" placeholder="Write today's thought… what you write shapes the plant."></textarea>
<div class="btns"><button class="primary" id="write">✎ Write entry</button>
<button id="w7">+1 week</button><button id="w30">+1 month</button></div>
${spec.note?`<div class="note" style="margin-top:8px">${spec.note}</div>`:''}
</div>
<div class="group"><h2>The three inputs</h2>
<div class="stat"><span>Maturity</span><b id="mVal">0%</b></div><div class="bar"><i id="mBar" style="width:0%;background:var(--accent)"></i></div>
<div class="stat"><span>Vitality</span><b id="vVal">100%</b></div><div class="bar"><i id="vBar" style="width:100%;background:#E8A200"></i></div>
<div class="stat"><span>Age</span><b id="aVal">0%</b></div><div class="bar"><i id="aBar" style="width:0%;background:#c47a4a"></i></div>
</div>
<div class="group"><h2>State</h2>
<div class="meta"><span>Stage</span><b id="stage">—</b><span>Entries</span><b id="entries">0</b>
<span>Age</span><b id="age">0 d</b><span>Season</span><b id="season">—</b>
<span>Status</span><span id="status" class="pill alive">alive</span></div>
</div>
<div class="group"><h2>Live traits</h2><div class="traits" id="traits"></div></div>
<div class="btns"><button id="play" disabled>▶ Memorial</button><button id="reset">↺ New seed</button></div>
<a class="dl" id="download" href="#">⤓ Download SVG</a>
</div>`;
const LIFESPAN=spec.LIFESPAN, DECAY=spec.DECAY||1/21;
let st, history;
const $=id=>document.getElementById(id);
function fresh(seed){ st={seed,artSeed:seed,entryCount:0,now:0,lastWatered:0,vitality:1,
wateredEpochs:0,lastWaterWeek:-1,diedAt:0,everWilted:false}; history=[snap()]; }
function snap(){ return {now:st.now,entryCount:st.entryCount,vitality:st.vitality,
wateredEpochs:st.wateredEpochs,artSeed:st.artSeed,diedAt:st.diedAt}; }
function paint(s){ $('art').innerHTML=renderPlant(spec, s||st); }
function refresh(){ paint();
const env=computeEnv(spec,st), age=clamp(st.now/LIFESPAN,0,1);
$('mVal').textContent=Math.round(env.m*100)+'%'; $('mBar').style.width=(env.m*100)+'%';
$('vVal').textContent=Math.round(st.vitality*100)+'%'; $('vBar').style.width=(st.vitality*100)+'%';
$('aVal').textContent=Math.round(age*100)+'%'; $('aBar').style.width=(age*100)+'%';
$('stage').textContent=spec.stageName?spec.stageName(st,env):defaultStage(spec,st,env);
$('entries').textContent=st.entryCount; $('age').textContent=Math.round(st.now)+' d';
$('season').textContent=seasonOf(st.now);
const dead=st.diedAt>0, status=$('status');
status.textContent=dead?('sealed · '+st.deathCause):'alive'; status.className='pill '+(dead?'dead':'alive');
$('play').disabled=!dead; ['write','w7','w30'].forEach(id=>$(id).disabled=dead);
const tw=$('traits'); tw.innerHTML='';
const tr=commonTraits(spec,st,env).concat(spec.traits?spec.traits(st,env):[]);
tr.forEach(([k,val])=>{const e=document.createElement('span');e.className='t';e.innerHTML=`<b>${k}</b> ${val}`;tw.appendChild(e);});
const blob=new Blob([renderPlant(spec,st)],{type:'image/svg+xml'});
$('download').href=URL.createObjectURL(blob); $('download').download='seedling-'+spec.title.toLowerCase().replace(/\W+/g,'-')+'.svg';
}
function checkDeath(){ if(st.diedAt>0)return;
// §3.2: deathTime monotonic in watered weeks — never moves earlier, so no retroactive death.
const dt=deathTimeOf(spec,st);
if(st.now>=dt){ st.diedAt=st.now;
st.deathCause = spec.MONOCARPIC ? 'MonocarpicBloom'
: (consistency(st)<0.6 ? 'CareShortened' : 'FullTerm'); } }
function writeEntry(){ if(st.diedAt>0)return;
const txt=$('entry').value.trim()||('e'+(st.entryCount+1)+'-'+Math.random());
st.artSeed=hashStr(st.artSeed+'|'+txt+'|'+st.entryCount).toString(16).padStart(8,'0');
st.entryCount++; st.lastWatered=st.now; st.vitality=1;
const wk=Math.floor(st.now/EPOCH); // distinct watered weeks (anti-gaming)
if(wk!==st.lastWaterWeek){ st.wateredEpochs++; st.lastWaterWeek=wk; }
$('entry').value=''; history.push(snap()); refresh(); }
function advance(d){ if(st.diedAt>0)return;
st.now+=d; st.vitality=vitalityOf(spec,st); // §3.1 exponential recency decay
if(st.vitality<0.25)st.everWilted=true;
checkDeath(); history.push(snap()); refresh(); }
let raf=null;
function memorial(){ if(history.length<2)return; cancelAnimationFrame(raf);
const F=history,n=F.length,DUR=5200,t0=performance.now();
(function step(t){ const p=clamp((t-t0)/DUR,0,1), fp=p*(n-1), i=Math.floor(fp), f=fp-i;
const A=F[i],B=F[Math.min(i+1,n-1)];
paint({seed:st.seed,artSeed:(f<0.5?A.artSeed:B.artSeed),entryCount:lerp(A.entryCount,B.entryCount,f),
now:lerp(A.now,B.now,f),vitality:lerp(A.vitality,B.vitality,f),
wateredEpochs:lerp(A.wateredEpochs,B.wateredEpochs,f),lastWatered:lerp(A.now,B.now,f),
diedAt:(p>=1?st.diedAt:0)});
if(p<1)raf=requestAnimationFrame(step); else refresh(); })(t0); }
function newSeed(){ fresh('0x'+Math.floor(Math.random()*0xffffffff).toString(16).padStart(8,'0')); refresh(); }
$('write').onclick=writeEntry; $('w7').onclick=()=>advance(7); $('w30').onclick=()=>advance(30);
$('reset').onclick=newSeed; $('play').onclick=memorial;
$('entry').addEventListener('keydown',e=>{if(e.key==='Enter'&&(e.metaKey||e.ctrlKey))writeEntry();});
// expose for headless/preview harnesses
window.__seedling={spec,renderPlant,maturity,computeEnv,get st(){return st;},set st(x){st=x;},refresh,fresh,schemeFor,settingName:null};
newSeed();
// URL params for gallery/preview: ?seed=..&e=entries&d=days&v=vitality&bare(hide panel)
const q=new URLSearchParams(location.search);
if(q.has('e')||q.has('seed')){
fresh(q.get('seed')||st.seed);
st.entryCount=q.has('e')?+q.get('e'):20; st.now=q.has('d')?+q.get('d'):220;
st.lastWatered=st.now; st.vitality=q.has('v')?+q.get('v'):0.92;
const weeks=Math.max(1,Math.floor(st.now/EPOCH)); // assume ≈1 watering/week up to entryCount
st.wateredEpochs=q.has('w')?+q.get('w'):clamp(st.entryCount,0,weeks);
st.lastWaterWeek=Math.floor(st.now/EPOCH);
st.notes=q.has('notes')?+q.get('notes'):0; // public notes → pollinators (§6.4)
if(st.now>=deathTimeOf(spec,st)){st.diedAt=st.now;st.deathCause='FullTerm';}
refresh();
}
if(q.has('bare')){ document.querySelector('.panel').style.display='none';
const stg=document.querySelector('.stage'); stg.style.padding='0';
document.querySelector('.stage svg').style.height='100vh'; }
// Studio/export support: when embedded in an iframe, PUSH the rendered SVG to the parent via
// postMessage. Origin-independent — works on file:// and in Safari, where the parent reading this
// iframe's contentDocument cross-origin throws SecurityError. token echoes ?_= so the parent can
// match this exact frame; harmless when no parent is listening.
try{ if(window.parent && window.parent!==window){
const tok=q.get('_')||'';
const emit=()=>{ const sv=document.querySelector('#art svg');
if(sv) window.parent.postMessage({seedlingSVG:1, token:tok, svg:sv.outerHTML}, '*'); };
emit(); requestAnimationFrame(emit);
} }catch(e){}
}
// ---------- exports ----------
const __api={ EPOCH,K_DEV,clamp,lerp,smooth,rng,hashStr,seasonOf,
consistency,maturity,structureOf,densityOf,densityCap,deathTimeOf,
vitalityOf,dormancyOf,pollinatorsOf,drawPollinator,computeEnv };
// headless (Node simulation harness):
if(typeof module!=='undefined'&&module.exports) module.exports=__api;
// browser namespace (so self-contained species can converge onto the corrected math
// without colliding with their own local helpers — see e.g. sunflower.html):
if(typeof window!=='undefined') window.SeedlingCore=__api;
;
/* boot.js */
/* ============================================================
Seedling — on-chain engine BOOT entrypoint.
Concatenated LAST into the engine bundle (after species-params.js,
seedling-core.js, and the per-species registry files). Defines the
single symbol the on-chain renderer calls:
SeedlingRenderer.boot(state)
`state` is the JSON object SeedlingRenderer.sol injects, mirroring the
clone's DiaryState (all times in SECONDS, seed/artSeed as 0x-hex):
{ tokenId, species, seed, artSeed, plantedAt, lastWatered,
diedAt, entryCount, wateredEpochs, deathTime }
boot() maps that to the engine's `st` (which counts in DAYS, EPOCH=7),
then renders + loops the life-cycle animation into the page.
============================================================ */
(function () {
var DAY = 86400;
var EPOCH_DAYS = 7;
// species index -> spec-extension ({ title, PAL, LW, LS, draw, stageName, … }),
// populated by the bundled bundle/species/*.js files (extracted from the prototypes).
var REGISTRY = {};
function registerSpecies(idx, ext) { REGISTRY[idx] = ext; }
// contract seconds -> engine st (DAYS). Mirrors the known-good prototype harness st.
function toEngineState(state, nowSec) {
var planted = Number(state.plantedAt) || 0;
var ageDays = Math.max(0, (nowSec - planted) / DAY);
var deathDays = planted > 0 ? Math.max(1, (Number(state.deathTime) - planted) / DAY) : 365;
var lastWaterDays = Number(state.lastWatered) > planted ? (Number(state.lastWatered) - planted) / DAY : ageDays;
var st = {
seed: state.seed,
artSeed: state.artSeed || state.seed,
entryCount: Number(state.entryCount) || 0,
wateredEpochs: Number(state.wateredEpochs) || 0,
now: ageDays,
lastWatered: lastWaterDays,
lastWaterWeek: Math.floor(lastWaterDays / EPOCH_DAYS),
vitality: 1,
notes: 0,
diedAt: 0,
deathCause: null,
};
if (Number(state.diedAt) > 0) {
st.diedAt = Math.max(0, (Number(state.diedAt) - planted) / DAY);
st.deathCause = "FullTerm";
}
return { st: st, deathDays: deathDays };
}
// Each species file registers its FULLY-RESOLVED spec (the prototype already merges
// SEEDLING_toSpec(SP) before registering), so boot just hands it to the engine as-is.
function specFor(species) {
return REGISTRY[species] || null;
}
function mountTarget() {
var el = document.getElementById("art");
if (!el) {
el = document.createElement("div");
el.id = "art";
document.body.appendChild(el);
}
// Fill the frame; the injected SVG is styled (below) to letterbox into ANY aspect.
// OpenSea renders animation_url in a SQUARE, but the art is portrait (720×980) — so the
// whole plant must fit via preserveAspectRatio=meet instead of overflowing → no scroll/crop.
el.style.cssText = "width:100vw;height:100vh;overflow:hidden";
if (!document.getElementById("seedling-fit")) {
var s = document.createElement("style");
s.id = "seedling-fit";
// Contain the whole portrait card into any frame; leftover space is TRANSPARENT (takes the
// host background — dark on OpenSea) so there are never cream bars. Portrait frames get no bars.
s.textContent =
"html,body{margin:0;background:transparent}#art{background:transparent}#art svg{display:block;width:100%;height:100%}";
document.head.appendChild(s);
}
return el;
}
// Honest placeholder until a species' draw() is registered (extraction is the remaining
// art work). Keeps the bundle runnable + the on-chain pipeline demonstrable today.
function placeholder(target, species, st) {
target.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" style="width:100%;height:100%">' +
'<rect width="600" height="600" fill="#f3efe6"/>' +
'<text x="300" y="290" text-anchor="middle" font-family="Georgia,serif" font-size="26" fill="#2c3a2c">Seedling engine</text>' +
'<text x="300" y="330" text-anchor="middle" font-family="monospace" font-size="16" fill="#5a6a5a">species ' +
species + " · entries " + st.entryCount + "</text></svg>";
}
function boot(state) {
var target = mountTarget();
var spec = specFor(Number(state.species));
var planted = Number(state.plantedAt) || 0;
var nowSec = planted > 0 ? Math.floor(Date.now() / 1000) : 0;
// Static frame (used when the species engine isn't bundled yet, or for a still).
var mapped = toEngineState(state, nowSec || planted);
if (!spec || typeof renderPlant !== "function") {
placeholder(target, Number(state.species), mapped.st);
return;
}
// One-way growth to NOW, then HOLD (art §7.5, revised). We sweep sprout → the plant's
// CURRENT age only — never fast-forward past it — so the art shows real growth-so-far and
// then freezes on the present frame. That held "now" frame is the still marketplaces poster
// (the "last image"); a living plant never renders senescence/death. For the pre-reveal
// teaser the contract feeds a lush mid-life state, so this grows a full green canopy + holds.
var DURATION = 4200; // ms sprout→now sweep (short so the held frame settles fast)
var targetDays = Math.max(0, mapped.st.now); // grow only up to the current age (NOW), never beyond
var start = null;
function frame(ts) {
if (start === null) start = ts;
var p = Math.min(1, (ts - start) / DURATION);
var eased = p * p * (3 - 2 * p); // smoothstep — gentle sprout, easy settle
var anim = toEngineState(state, planted + eased * targetDays * DAY).st;
target.innerHTML = renderPlant(spec, anim);
if (p < 1) requestAnimationFrame(frame); // stop at NOW → holds the last image (no loop, no death)
}
requestAnimationFrame(frame);
}
// public entrypoint + registry hook for the bundled species files
if (typeof window !== "undefined") {
window.SeedlingRenderer = { boot: boot, registerSpecies: registerSpecies, _registry: REGISTRY };
}
})();
;
/* species/aloe.js */
/* aloe — species index 2. GENERATED from prototypes/aloe-vera.html by extract.js.
Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's
mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */
(function () {
const mount = (spec) => SeedlingRenderer.registerSpecies(2, spec);
/* Aloe Vera — a stemless rosette of thick succulent toothed leaves: it starts as a 2-leaf V, builds
a narrow distichous fan, then opens into a spiral vase-rosette (newest leaves central & upright,
oldest outer, long & recurved). A mature rosette throws up several thin green RACEMES, each topped
by a cone of small coral florets that colour from the base up (green bud tip). Clonal PUPS crowd the
base; clumps via Setting. Hardy; wind barely leans it. Ported to the shared core. */
const SP = window.SEEDLING_SPECIES.find(x=>x.key==='aloe');
mount(Object.assign(window.SEEDLING_toSpec(SP), {
title:'Aloe Vera', sub:'aloe v9',
senFrom:0.82,
LW:{season:1.0, stem:1.2, leaves:1.25, flowers:1.45},
LS:{season:1.0, stem:1.0, leaves:0.9, flowers:0.7},
note:'Succulent rosette (2-leaf V → fan → spiral vase) · toothed flecked blades · fanned racemes of coral florets (colour rises from the base) · clonal pups · clumps.',
PAL:{ soil:'#6f5128', dry:'#7a5a30',
leaf:'#79A074', leafDk:'#496B47', leafLt:'#A7C58D', tooth:'#3A5638', fleck:'#E2EFD2',
bud:'#8FA653', budDk:'#5E7338', stalk:'#7E9B54',
flower:'#E8602E', flowerDk:'#C5331A', flowerTip:'#F4C24A',
capsule:'#90A158', capsuleDry:'#9C7E40' },
stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m;
if(env.ageF>0.85)return'Senescent'; if(m<0.10)return'Sprout'; if(m<0.34)return'Fan';
if(m<0.58)return'Rosette'; if(m<0.78)return'Mature'; return'Flowering'; },
draw(st, C, env){
const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL;
const bgC = BACKDROPS[backdropFor(st.seed)], GA=Math.PI*(3-Math.sqrt(5));
const seasonBloom = (season==='Winter'||season==='Spring')?1 : season==='Autumn'?0.7 : 0.58;
function aloeDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5;
return { leafN:0.8+0.55*dd(), fat:0.65+0.6*dd(), recurve:0.45+0.85*dd(), twist:0.5+0.9*dd(), teeth:0.45+0.7*dd(),
flecks:dd(), stature:0.85+0.30*dd(), flowerHue:g()*0.5, nRaceme:dd()<0.62?1:2, pups:0.25+0.75*dd(),
windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.4),
// — posture DNA — the rosette spiral used to be globally one-handed (sin(i*GA) is seed-independent),
// so EVERY plant's crown drifted the same way → "always leans right". These break that symmetry:
chir:dd()<0.5?1:-1, // spiral handedness — flips which way the phyllotaxis winds
spiralPh:dd()*TAU, // per-seed phyllotaxis phase → a different wobble signature each seed
lean:g(), // bell-weighted BIDIRECTIONAL posture (most upright; some tilt L or R)
leanCurve:0.6+0.8*dd(), // how strongly the lean reads through blade curvature vs a rigid tilt
fullSide:dd()<0.5?1:-1, // which flank carries fuller / longer blades (no rosette is a mirror)
fullAmt:Math.pow(dd(),1.3) }; // strength of that lopsidedness (skewed subtle, occasionally bold)
}
function paintAloe(I){
const {seed, artSeed, cx0, by, S, fade, m, density, v, sen, browny, rewards} = I;
const FC = c => fade>0 ? mix(c, bgC, fade) : c;
const ra=rng('struct',artSeed
A living on-chain diary you grow as a plant. Write to water it; `neglect wilts it; every diary is` mortal. Fully on-chain, eternal`data:application/json;base64,A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``r{"name":"Seedling #`h - Revealing Soon","description"`3","attributes":[{"trait_type":"Status","value":"Revealing Soon"}` a ,V[o","attributes":[`{"trait_type":"Species","value":`{"trait_type":"Rarity","value":"`{"trait_type":"Status","value":"`{"trait_type":"Entries","value":`{"trait_type":"Watered Weeks","v`A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``,"diedAt":0,"entryCount":24,"wateredEpochs":20,"deathTime":<!doctype html><html><head><meta` <meta name="viewport" content="w`Qidth=device-width,initial-scale=`q<style>html,body{margin:0;height`:100%;background:transparent}</s`</script><script>SeedlingRendere)</script></body></html>RlGolden Wattle`RoBird of Paradise`RlChilean Guava`RmJapanese Maple`RsAntarctic Hair Grass``ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` data:image/svg+xml;base64,<svg xmlns="http://www.w3.org/20` 00/svg" viewBox="0 0 600 600"><rect width="600" height="600" f`^<rect x="30" y="30" width="540" `height="540" fill="none" stroke=`"#3a4a3a" stroke-width="2"/><text x="60" y="110" font-family`="Georgia,serif" font-size="40" a<text x="60" y="160" font-family`="Georgia,serif" font-size="22" `'<text x="60" y="520" font-family`="monospace" font-size="20" fill`'<text x="60" y="550" font-family`="monospace" font-size="20" fill`'="#5a6a5a">watered weeks
# Seedlings — Encryption Spec (client-side; open-source + etched on-chain)
The exact, frozen recipe for reading/writing a Seedling diary. Per contract spec §6
this is **open-sourced from day one AND etched on-chain (SSTORE2)** alongside the
renderer, so any community app can read/write diaries and they stay decryptable even if
every frontend (including ours) disappears. **The chain carries the instructions for
reading itself.**
> ✅ **LOCKED (2026-06-20) — single launch scheme, post-quantum from day one.** Algorithm
> choices below are final, implemented in the **reference client** (`../reference-client/`,
> audited `@noble/*` primitives), and pinned with **test vectors**
> (`../reference-client/test/test-vectors.json`) the client reproduces byte-for-byte in CI,
> and **etched on-chain via SSTORE2** (`EncryptionSpec.sol`, with keccak integrity anchors).
> Nothing here runs on-chain — the chain only ever sees ciphertext + opaque wrapped-key blobs.
>
> **Post-quantum:** the only quantum-exposed surface is the asymmetric **key wrap** (§4), and
> its blobs are public-forever on-chain (worst case for "harvest-now-decrypt-later"). The wrap
> is therefore a **hybrid KEM = X25519 + ML-KEM-768 (FIPS 203)** whose wrap key binds BOTH
> shared secrets → secure as long as **either** primitive holds. Entry encryption (AES-256-GCM,
> §3) and `eraKey` derivation (HKDF, §1) are symmetric and were already post-quantum-safe →
> **unchanged**. There is no separate "v1/v2": this is the one scheme, with a `SCHEME` version
> byte reserved for any future migration.
>
> **Locked byte conventions (the parts a second implementer must not guess):**
> - **`SIG`** = the **65-byte** EIP-191 `personal_sign` signature (`r‖s‖v`, `v ∈ {27,28}`);
> `seedBits = keccak256(SIG)`.
> - **HKDF** = HKDF-SHA256 (extract+expand). String **salts/info are their UTF-8 bytes**;
> `cloneAddress` (as HKDF `info` for the era key, and as AAD) is the **20 raw address bytes**
> — never a hex string, never checksummed. Output length is 32 except the ML-KEM keygen seed
> (length **64**).
> - **AES-256-GCM**: 12-byte IV (`nonce`/`wn`), 16-byte tag appended after the ciphertext.
> - **X25519** (classical half): the 32-byte HKDF output is the scalar (clamped internally);
> `xPub = X25519_base(xPriv)`.
> - **ML-KEM-768** (PQ half, FIPS 203): keygen is **seeded** (64-byte HKDF output) so keys are
> stateless/re-derivable; sizes pk **1184**, ct **1088**, shared secret **32**, encaps msg **32**.
> - **`SCHEME` = `0x01`** is the leading version byte of both the published `pubKey` and the
> wrap `blob` (`0x01` = hybrid X25519+ML-KEM-768).
> - All `‖` are byte concatenations.
---
## 0. Model — one key PER OWNER ERA (not one key for the whole diary)
The diary's life is a sequence of **ownership eras** (owner A, then B, then C…),
delimited by the deed's `Transfer` events. **Each owner encrypts the entries they write
under their own `eraKey`.** This is the privacy backbone:
- An owner can **always re-read their own era's entries** (they can re-derive their
`eraKey` from their wallet signature) — even long after they've sold. *Their* words.
- An owner **cannot read another era's entries** unless that era's owner **hands them the
key**. In particular a past owner can **never** read a *future* owner's entries — they
never hold a later `eraKey`. Selling truly hands over the future; the past stays the
seller's to share or keep.
- The **current owner** reads/writes their own era, owns the token + art, and may
**receive past eras' keys** from past owners to read the diary's history.
(A naïve "one key for the whole diary" would let a past owner read the buyer's *future*
entries — a leak. Per-era keys close that.)
**Trust:** sharing the past is **opt-in and open-ended** (revelation-on-sale, §6) — any
past owner may hand their `eraKey` to the current owner **at any time**, or never. The
contract provides the rail; it never coerces and never sees plaintext or private keys.
**Phishing (documented):** the derivation signature **is** the key. Anyone who obtains it
reads that era. Mitigations: the signed message is a self-describing warning (below), it
is **never reused** for login/verification, and open-source clients are the audit surface.
## 1. Key derivation (client-side, deterministic, per era)
Each user derives, from one wallet signature:
- a **hybrid encryption keypair `EK`** (X25519 + ML-KEM-768) — its public half (`pubKey`) is
published on-chain so others can wrap keys to them;
- their **`eraKey`** for a given diary — the AES key for entries they write in it.
```
SIG = personal_sign(account, MESSAGE) // EIP-191
seedBits = keccak256(SIG) // 32 bytes
// classical half (X25519)
xPriv = HKDF-SHA256(seedBits, salt="seedling/ek/v1", info="x25519", 32) // clamp per X25519
xPub = X25519_base(xPriv)
// post-quantum half (ML-KEM-768, FIPS 203) — seeded keygen ⇒ stateless
kemSeed = HKDF-SHA256(seedBits, salt="seedling/ek/v1", info="ml-kem-768", 64)
(kemPub, kemPriv) = ML-KEM-768.KeyGen(kemSeed)
// published, version-tagged hybrid public key (this is what registerEncryptionPubKey stores)
pubKey = 0x01 ‖ xPub(32) ‖ kemPub(1184) // 1217 bytes
// per-diary symmetric era key (UNCHANGED — already PQ-safe)
eraKey = HKDF-SHA256(seedBits, salt="seedling/era/v1", info=cloneAddress, 32) // AES-256, per diary
```
`MESSAGE` (exact bytes, **never** reused elsewhere — frozen at v1):
```
Seedling diary key — v1
This signature IS the encryption key to your diary. Anyone who gets this
signature can read everything you write here, forever, and you cannot undo
it. Only sign this in the official or an open-source Seedling client you
trust. Never sign it to "verify", "log in", or "connect".
```
All of these are **stateless** — re-deriving from the same account (incl. ML-KEM's seeded
keygen) always yields the same keys; no key storage needed. `registerEncryptionPubKey(pubKey)`
publishes the version-tagged hybrid `pubKey` (once per user, updatable).
## 2. Lifecycle
- **Mint / first write:** owner A derives `eraKey_A` and encrypts their entries with it.
(No key blob needs committing for *their own* reading — they re-derive it. Blobs are
only for sharing with *others*.)
- **Sale (A → B):** on `KeyHandoffRequested`, A's client may **hybrid-wrap `eraKey_A` to B's
published `pubKey`** and `commitWrappedKey(B, blob)` — now and/or anytime later, A's choice. B
writes their own entries under `eraKey_B`.
- **Read (owner X):** the client lists entries from the logs, maps each to its era via
the `Transfer` timeline (entry timestamp ↔ which owner held the deed then), and decrypts
each with the matching `eraKey` **if X holds it** (their own, re-derived; others', via a
`KeyCommitted` blob X can unwrap). Eras X lacks keys for render as "still sealed by a
previous keeper."
- **Open-ended history:** any past owner may, at any future time, `commitWrappedKey` their
era key to whoever owns the diary then — retroactively unlocking that era for them.
## 3. Entry encryption (per entry, in the log payload)
```
nonce = 12 random bytes (per entry)
ct, tag = AES-256-GCM-Encrypt(key=eraKey_of_writer, iv=nonce, aad=AAD, plaintext=UTF8(text))
payload = 0x01 ‖ nonce(12) ‖ ct ‖ tag(16) // 0x01 = encrypted format/version
```
- `AAD = abi.encodePacked(cloneAddress)` (binds ciphertext to the diary).
- `contentHash` stored with the log = `keccak256(payload)` → feeds the on-chain rolling
`artSeed` integrity chain (§3.2). Integrity is era-independent (covers all entries).
- **Public entries** (`isPublic = true`): `payload = 0x00 ‖ UTF8(text)` (plaintext, never
encrypted), same 495-byte cap. The `0x00`/`0x01` version byte tells a reader whether to
decrypt; public entries are always readable by anyone, no key needed.
- **495-byte budget** is the *payload* length (~≤466 plaintext after the 29-byte AEAD
overhead). The contract caps `payload.length ≤ 495`.
## 4. Key wrapping (`eraKey` → a recipient) — hybrid KEM (X25519 + ML-KEM-768)
The wrap encapsulates to **both** halves of the recipient's `pubKey` and binds **both** shared
secrets into the wrap key, so it stays secure if **either** X25519 or ML-KEM-768 holds. The
combiner is an explicit application-layer HKDF (not a library "hybrid" module), so any second
implementer can reproduce it byte-for-byte.
```
xPub, kemPub = parse(recipient pubKey) // strip 0x01, split 32 ‖ 1184
eph_priv, eph_pub = X25519 keypair (random, per wrap)
ss_x = X25519(eph_priv, xPub) // classical shared secret (32)
ct_kem, ss_kem = ML-KEM-768.Encapsulate(kemPub) // PQ ciphertext (1088) + shared secret (32)
wrapKey = HKDF-SHA256( ikm = ss_x ‖ ss_kem, // secure if EITHER holds
salt = eph_pub ‖ ct_kem ‖ xPub ‖ kemPub, // bind both cts + both recipient keys
info = "seedling/wrap/v1", 32 )
wn = 12 random bytes
wct, wtag = AES-256-GCM-Encrypt(key=wrapKey, iv=wn, plaintext=eraKey)
blob = 0x01 ‖ eph_pub(32) ‖ ct_kem(1088) ‖ wn(12) ‖ wct(32) ‖ wtag(16) // 1181 bytes
```
Unwrap reverses it with the recipient's hybrid private key: `ss_x = X25519(xPriv, eph_pub)`,
`ss_kem = ML-KEM-768.Decapsulate(ct_kem, kemPriv)`, recompute `wrapKey`, AES-GCM-decrypt. The
sender never learns the recipient's private key; the recipient never reveals it. The blob
**self-validates** — a wrong recipient yields the wrong `wrapKey` (ML-KEM's implicit rejection
gives a junk `ss_kem` rather than an error), so the AES-GCM tag check simply fails. An
open-ended/permissionless commit is therefore safe; the on-chain gate only requires the
committer to be a past/current owner (to limit spam).
## 5. On-chain rail (what the contracts provide — contract spec §6)
| Piece | Where | Purpose |
|---|---|---|
| `registerEncryptionPubKey(bytes pubKey)` | mother | publish a user's 1217-byte hybrid `pubKey` (event `EncryptionPubKey`) |
| `wasEverOwner(tokenId, who)` | mother | gate: only a past/current owner may hand off a key |
| `KeyHandoffRequested(tokenId, from, to)` | clone (on transfer) | prompt the seller's client to re-wrap |
| `commitWrappedKey(forOwner, blob)` | clone | carry a wrapped `eraKey`; **blob persisted to SSTORE2** (state, not log) → returns + emits the pointer in `KeyCommitted(tokenId, forOwner, pointer, ts)`; open-ended |
| `keyBlobCount/keyBlobPointer/keyBlob(forOwner[,i])` | clone | read committed blobs back from state (eternal availability) |
The contracts **never validate** the crypto and never see plaintext/private keys — they
carry opaque blobs + signals. All correctness lives in the reference client + this spec.
Writing/growth is gated to the **current owner only**, separately — a key-handoff
permission never grants write access.
**Durability (why the blob lives in state, not a log):** a handoff may target a *future*
owner who only appears years later (open-ended history). For **encrypted** data the wrapped
key **is** availability — there is no plaintext archive to fall back on, so if the only
copy were an event it could be lost to EIP-4444 history expiry and that era would become
permanently unreadable. `commitWrappedKey` therefore writes the blob to **SSTORE2** (every
node keeps it forever, like the etched entries §3.4 and this spec itself); the
`KeyCommitted` event carries only the **pointer** as a cheap index. A reader fetches a
blob with `keyBlob(forOwner, i)` (or `eth_getCode(pointer)[1:]`) and unwraps it (§4)
entirely client-side. The blob byte format (§4) is unchanged.
## 6. Test vectors (✅ pinned)
Pinned in **`../reference-client/test/test-vectors.json`** and reproduced byte-for-byte by
the reference client (`npm test` in `reference-client/`). Covered:
- **`SIG → EK / eraKey` derivation** — two users (A, B) from fixed test private keys, each
signing the exact `MESSAGE`; pins `signature`, `seedBits`, `xPriv`, `xPub`, `kemPub`, the
version-tagged `pubKey`, and `eraKey`.
- **Entry encrypt/decrypt** with a known nonce → the full `0x01 ‖ nonce ‖ ct ‖ tag` payload
+ its `contentHash` (= `keccak256(payload)`), round-tripped.
- **Public-entry no-op** → `0x00 ‖ UTF8(text)`, decoded with no key.
- **`eraKey` hybrid wrap/unwrap** with fixed ephemeral key + ML-KEM encaps msg + nonce → the
1181-byte blob, and the revelation-on-sale flow (B unwraps A's era key, then reads A's entry).
- **Negatives** (the privacy guarantees): wrong era key fails; AAD binds to the diary (a
different `cloneAddress` fails); a stranger (wrong hybrid keypair) can't unwrap a blob wrapped
to B; the 495-byte payload cap is enforced.
Regenerate with `npm run vectors` (deterministic). ✅ **Etched on-chain:** `EncryptionSpec.sol`
SSTORE2-writes this doc **and** the vectors as contract code, with keccak integrity anchors,
deployed by `Deploy.s.sol` — the chain carries the instructions for reading itself.
{
"_spec": "ENCRYPTION_SPEC.md — hybrid X25519 + ML-KEM-768 (single launch scheme)",
"_warning": "TEST-ONLY keys/nonces. Never use for a real diary.",
"message": "Seedling diary key — v1\n\nThis signature IS the encryption key to your diary. Anyone who gets this\nsignature can read everything you write here, forever, and you cannot undo\nit. Only sign this in the official or an open-source Seedling client you\ntrust. Never sign it to \"verify\", \"log in\", or \"connect\".",
"cloneAddress": "0x00000000000000000000000000000000000000c1",
"derivation": {
"description": "SIG (65-byte EIP-191 personal_sign of `message`) → hybrid keys; pubKey = 0x01 ‖ xPub(32) ‖ kemPub(1184)",
"userA": {
"privateKey": "0x1111111111111111111111111111111111111111111111111111111111111111",
"signature": "0x30ee322abba50eafce6e0ee26170af995e2c6eaa9b75f4fe3abddfb8483ff78a5723863fc0a50a6f70a1a06b47594c1df30a1e40a2713fe494b122e10cb5093c1b",
"seedBits": "0xc40b58775af20626866a56543e825a1a7cc1260fbe6c41d42a4129e3e0520aa8",
"xPriv": "0xae0a5cf044a52600e8586223625d441d9481357595af70f19712506c83441ac1",
"xPub": "0x9257a06ecb98575fc619a6eea8301bba0dcfa86e3c1cd5128702f02409821773",
"kemPub": "0x8ba58ce7b29dd1562fbcc575ca969a13386d84b33cce8b6c6b9523b941c5be73938c502dfefac1949708644014d34260325379919c2f3ec1ada0f2ccdfc700ffe29a16ca2b08849eb4f14931d49590eb06f4b32bc040abd9032e25168707528425f9421a4bb1bb510546c9030130915280af212657732a24863b0b727a1b50ab320f7846abbb2e231b9c2818afb6ac1b8243b37759ae6c9a80efc12ae919a90a772f63f7addfc45b3865b1c6335c2a9b23e7782118b46c46786de18ab77a448f8b7a5e2ce3b27176050110547e19450dd27086f94f5ff887eac201ba394978ca8216b59dd7d0517ab57bd4039956572043a887a9ea4a1d2ba0d76bbfee268b098256150927dc9234982c431e2767bd3a1962da2b1f545f650440c8ca02a5336570c46855c296e9fb2cf51a5a13503c882279d0b968d8b1773a38615687addb78950f37617a635e71c299d44b52515a93246cb249d76f9bc010bd47b59bba1091b946834a8cebd986582061ebf21d73b69b8667b9ad2a0cd2a9530b9475f16b17e4f0c3d7e19bb0f547743cba7b543243d44e4b361a9386c36d47b999a177020215f10472791cb5073194e8345ae4874cdf172482f1625125b3d720cc1dfa9cc84a8905c723f4c6c531a83a4a8061fb0a2faa9032640bc1beb17cca50ca7cda1851766f27699140d6478b64c4d732aca47bb97aa0b23fdaa90abc131eb86a05c1a76cc71216c286fc92c68cdb8b1a875b06dac19c8ab229d07a6506b77442b047117d89a48e7b460f622765b9c464051a283b6449ebbb2eebe2aefdf802cd9abc70a092154362f69353b7f12afd7411ea84a981894a52559b5f0cc2a8f7974932bd002018a231c8e5b471edd14cf7610aee1a91cbeaaee4742db17678ce0a358b34b1565446e63894e5b17638a9a4443c2dc10764b056a929e489b7684acc83cfc3b8ba906a3879983658078739d22c7db69373629d1c13cb5e4c72ea699b9029bdfa6a2ace5baac8e646a93590488cbbc568325fd01bb03b21a245cf4f1637580b9fa021851d97b02a636e0f589adbba0b7669c59347b973d14d4b13a7dbe8116c840e260192e35373fe3094dfb553e869bf8b0876182985c7002ce7b21e3900be31ab04ea2004a8b5b4b152a2fcb28893046100902bc2002f19195e3ffb2c0d1b0347b961f757a94b3b7687362512f8873a2c1482395f5e485aa81c64f767b66c7ac86baca48a07ceb8a4188a6932199c7c96440104812c4b1026f11871de64ce7062435d4302d566ad9bdc8885e27227a4742a586266fa1d1ab66059f07f3002cfe71363f25c4f7a59bcebe025717c01a51079053baa6aeb2ccbc0bbc9866079934720161d736a6f931a90ad3a885a5ca935810a8fc2902cf57bf3f5344227c3074a4530768dda9bc8d387317159810577b5cbb384ab426840507d7302b7e3a21252573999fa941f9183c1a6b313d640f30601b55024999c6f5b5c9312dab9a6a85ac6b57f9ad4511618564c22b122167db4785ab011124d90b6fd956b8b453cc7221163a14cf94a919d63c2bed8606f9c38c734253d217b8d1cb89bb3b1b2e3cc87a34f4d2b5da935bc0db3bd40d1920e09bbbc03938f5b519d8383a167a200758284caff2161859ebfdca871be22e8a792f668b13473373bc9377a698abc",
"pubKey": "0x019257a06ecb98575fc619a6eea8301bba0dcfa86e3c1cd5128702f024098217738ba58ce7b29dd1562fbcc575ca969a13386d84b33cce8b6c6b9523b941c5be73938c502dfefac1949708644014d34260325379919c2f3ec1ada0f2ccdfc700ffe29a16ca2b08849eb4f14931d49590eb06f4b32bc040abd9032e25168707528425f9421a4bb1bb510546c9030130915280af212657732a24863b0b727a1b50ab320f7846abbb2e231b9c2818afb6ac1b8243b37759ae6c9a80efc12ae919a90a772f63f7addfc45b3865b1c6335c2a9b23e7782118b46c46786de18ab77a448f8b7a5e2ce3b27176050110547e19450dd27086f94f5ff887eac201ba394978ca8216b59dd7d0517ab57bd4039956572043a887a9ea4a1d2ba0d76bbfee268b098256150927dc9234982c431e2767bd3a1962da2b1f545f650440c8ca02a5336570c46855c296e9fb2cf51a5a13503c882279d0b968d8b1773a38615687addb78950f37617a635e71c299d44b52515a93246cb249d76f9bc010bd47b59bba1091b946834a8cebd986582061ebf21d73b69b8667b9ad2a0cd2a9530b9475f16b17e4f0c3d7e19bb0f547743cba7b543243d44e4b361a9386c36d47b999a177020215f10472791cb5073194e8345ae4874cdf172482f1625125b3d720cc1dfa9cc84a8905c723f4c6c531a83a4a8061fb0a2faa9032640bc1beb17cca50ca7cda1851766f27699140d6478b64c4d732aca47bb97aa0b23fdaa90abc131eb86a05c1a76cc71216c286fc92c68cdb8b1a875b06dac19c8ab229d07a6506b77442b047117d89a48e7b460f622765b9c464051a283b6449ebbb2eebe2aefdf802cd9abc70a092154362f69353b7f12afd7411ea84a981894a52559b5f0cc2a8f7974932bd002018a231c8e5b471edd14cf7610aee1a91cbeaaee4742db17678ce0a358b34b1565446e63894e5b17638a9a4443c2dc10764b056a929e489b7684acc83cfc3b8ba906a3879983658078739d22c7db69373629d1c13cb5e4c72ea699b9029bdfa6a2ace5baac8e646a93590488cbbc568325fd01bb03b21a245cf4f1637580b9fa021851d97b02a636e0f589adbba0b7669c59347b973d14d4b13a7dbe8116c840e260192e35373fe3094dfb553e869bf8b0876182985c7002ce7b21e3900be31ab04ea2004a8b5b4b152a2fcb28893046100902bc2002f19195e3ffb2c0d1b0347b961f757a94b3b7687362512f8873a2c1482395f5e485aa81c64f767b66c7ac86baca48a07ceb8a4188a6932199c7c96440104812c4b1026f11871de64ce7062435d4302d566ad9bdc8885e27227a4742a586266fa1d1ab66059f07f3002cfe71363f25c4f7a59bcebe025717c01a51079053baa6aeb2ccbc0bbc9866079934720161d736a6f931a90ad3a885a5ca935810a8fc2902cf57bf3f5344227c3074a4530768dda9bc8d387317159810577b5cbb384ab426840507d7302b7e3a21252573999fa941f9183c1a6b313d640f30601b55024999c6f5b5c9312dab9a6a85ac6b57f9ad4511618564c22b122167db4785ab011124d90b6fd956b8b453cc7221163a14cf94a919d63c2bed8606f9c38c734253d217b8d1cb89bb3b1b2e3cc87a34f4d2b5da935bc0db3bd40d1920e09bbbc03938f5b519d8383a167a200758284caff2161859ebfdca871be22e8a792f668b13473373bc9377a698abc",
"eraKey": "0xe3c8fbf51f290cc62603ab8fef847a08cd172fd9cd57179435ba9671ab5a0c58"
},
"userB": {
"privateKey": "0x2222222222222222222222222222222222222222222222222222222222222222",
"signature": "0x1e665df522a3c5d47200898f16f3bd0d79ade3abcc1bd53a74fbfba49c493bb554dc49145f404aa70d5bac41891bfd6a3c93160e6535e95e45ac73b9cf86c1f31c",
"seedBits": "0x048212e4f4056c25a07544fcabd39792e520f56762d40849fc9c1d6ab4ac8736",
"xPriv": "0x51a15d67d3adf63f7883ed0c64bf121179546a3c982f5921ea1e6a4dd8dc6f2d",
"xPub": "0x92ccc54d8600c7f19cf6e3364673d0e8e0cb78895b17599d1cf9edd0c25c453b",
"kemPub": "0xadd23581526d80911ae78b9f5df0521d0590046a73af152701cab464b30a0d91b4e0f778cf726cdffc73f6d1599a39732945b21c74648cfb28c5e62de25ac2fb83545ff3736a5068ca632a7d28820ce14c8ad1ae6b78011af55533713d74a9b9bc68af113076791c791c0076abb4217c218881e5b0cda69896765f00b131b5e22f5b33321c2b7057b85ce10266206b372758b987fc01dfc1770cdcbaf7b4944ea4458a2a615a3a842ab9983c830bdf198481437adc727a378abe56824338c3c72ea9a87fda3addfc583d6380b0b8354603647fd9215b2aa746b64659a82dee4a8dd91b0d56581424da22e2b18ff9382fef652356ca1203c7799a3206e53671f8b90df6823cf2a46dffa0aa8af6c4ffe67406abaaa8495ffcd1680b28248c92501ee370cb9bb69c9915ad7c6cf7733d1e14c95c11187fa69208497a61b5c0ea0c8d778a9362b1bcd7141fb18a3055ba8b26fc88c104240a467b8866a099d090accbb4c2a279b04b1f7201123f63ab10a0c0bc029f6308722b96761b0c079e6435fe0bc5c3226f23100ee8f6b620d21d2b8c0882c56f38001862b05a4686be627279bb997c3bd96ec2f042db60bf7fc427e7511454d49c60d23173dcc375a75bdf8397f1e24df4f28ea35ca840a2a7b0240b42c00114a600ffb00ebd2b204f148bbb31549459684fbc5ef0ca638d80b581027ff5ca9d4d66cb1467507dd53ecae303dae3ce244511c166b1131ba2129c8a50aa20b0284553fbc1acd47e9279052c36478397bb51fb01d52a27c0a3a88df728c709cbcd79b6fecc818d5bb9303a666986c4e7831f1be2a673b91b6e345c375ab7e1837abe25946543948c4233b926628856b1013093143431165b889cec63132946248b7fa54473682b9e7979632527879ec8b686bbc7381b48e7c3bccf2c74af067d86fb302a02cd3001063ba146bcfccb45272e7fc8715d8b5969f7bd5f4901f17ac611742e8159002299bed4db830e902a817a3cd3051ea38a6a08394593783eb580586e3b468bc51b47e712be1c36621908330c59cc3158045739efb787d3cabb56944ec7d82484ac9d992b44451c66ba7542eff11b0c8cbf72d95394755f33f8bdf6b79dfa27aa7e567fc7f93e4ba6cfc47a248c8aa2bf667f0b42b9f7d08acb0cadb914417c225290150c307c357707bf6b2880ebac9561604a33f782ebb4872802834b47abeed192b1290edf2b1c0de6618728473f6b9cb5eb4ffff168371935e37cbb2143998dea8347f05f264a20b7c576778635364cb03c7a86b8a95dc19083dd715111ba0ff8c7c39b3948811c09580bafa332af1354a857e85694359140fa1eaf3224ef65adc42109eef4a7f099c5521c1491b991c075429ae44ca569378cb33caa463afa117515562dedd2c2e782a4c08306e313460df251998bc1656883fe3852b9c2af7b1a3cc6d3032a8ab6956a42c2c6431172686b4337551cad2540af1df93560f74e211804d4a5ce64046f5d24857101c8a25c121724c6a3332034645d230a21303252831c3855a590586a2a22eb2519460b0e885f925b3dfca7cb023963084c3284b2a45a57341f4a1d8a6a69b6ba0d908a138d83942cfa1003fa745ac72d3f1bac5724bfa4fcee88f83d8cc089a44ebbd2d9813c68e11a129a3cc3683156ef152b7bf9ec",
"pubKey": "0x0192ccc54d8600c7f19cf6e3364673d0e8e0cb78895b17599d1cf9edd0c25c453badd23581526d80911ae78b9f5df0521d0590046a73af152701cab464b30a0d91b4e0f778cf726cdffc73f6d1599a39732945b21c74648cfb28c5e62de25ac2fb83545ff3736a5068ca632a7d28820ce14c8ad1ae6b78011af55533713d74a9b9bc68af113076791c791c0076abb4217c218881e5b0cda69896765f00b131b5e22f5b33321c2b7057b85ce10266206b372758b987fc01dfc1770cdcbaf7b4944ea4458a2a615a3a842ab9983c830bdf198481437adc727a378abe56824338c3c72ea9a87fda3addfc583d6380b0b8354603647fd9215b2aa746b64659a82dee4a8dd91b0d56581424da22e2b18ff9382fef652356ca1203c7799a3206e53671f8b90df6823cf2a46dffa0aa8af6c4ffe67406abaaa8495ffcd1680b28248c92501ee370cb9bb69c9915ad7c6cf7733d1e14c95c11187fa69208497a61b5c0ea0c8d778a9362b1bcd7141fb18a3055ba8b26fc88c104240a467b8866a099d090accbb4c2a279b04b1f7201123f63ab10a0c0bc029f6308722b96761b0c079e6435fe0bc5c3226f23100ee8f6b620d21d2b8c0882c56f38001862b05a4686be627279bb997c3bd96ec2f042db60bf7fc427e7511454d49c60d23173dcc375a75bdf8397f1e24df4f28ea35ca840a2a7b0240b42c00114a600ffb00ebd2b204f148bbb31549459684fbc5ef0ca638d80b581027ff5ca9d4d66cb1467507dd53ecae303dae3ce244511c166b1131ba2129c8a50aa20b0284553fbc1acd47e9279052c36478397bb51fb01d52a27c0a3a88df728c709cbcd79b6fecc818d5bb9303a666986c4e7831f1be2a673b91b6e345c375ab7e1837abe25946543948c4233b926628856b1013093143431165b889cec63132946248b7fa54473682b9e7979632527879ec8b686bbc7381b48e7c3bccf2c74af067d86fb302a02cd3001063ba146bcfccb45272e7fc8715d8b5969f7bd5f4901f17ac611742e8159002299bed4db830e902a817a3cd3051ea38a6a08394593783eb580586e3b468bc51b47e712be1c36621908330c59cc3158045739efb787d3cabb56944ec7d82484ac9d992b44451c66ba7542eff11b0c8cbf72d95394755f33f8bdf6b79dfa27aa7e567fc7f93e4ba6cfc47a248c8aa2bf667f0b42b9f7d08acb0cadb914417c225290150c307c357707bf6b2880ebac9561604a33f782ebb4872802834b47abeed192b1290edf2b1c0de6618728473f6b9cb5eb4ffff168371935e37cbb2143998dea8347f05f264a20b7c576778635364cb03c7a86b8a95dc19083dd715111ba0ff8c7c39b3948811c09580bafa332af1354a857e85694359140fa1eaf3224ef65adc42109eef4a7f099c5521c1491b991c075429ae44ca569378cb33caa463afa117515562dedd2c2e782a4c08306e313460df251998bc1656883fe3852b9c2af7b1a3cc6d3032a8ab6956a42c2c6431172686b4337551cad2540af1df93560f74e211804d4a5ce64046f5d24857101c8a25c121724c6a3332034645d230a21303252831c3855a590586a2a22eb2519460b0e885f925b3dfca7cb023963084c3284b2a45a57341f4a1d8a6a69b6ba0d908a138d83942cfa1003fa745ac72d3f1bac5724bfa4fcee88f83d8cc089a44ebbd2d9813c68e11a129a3cc3683156ef152b7bf9ec"
}
},
"entryEncrypt": {
"description": "AES-256-GCM private entry, fixed nonce → 0x01 ‖ nonce(12) ‖ ct ‖ tag(16)",
"eraKey": "0xe3c8fbf51f290cc62603ab8fef847a08cd172fd9cd57179435ba9671ab5a0c58",
"nonce": "0xa0a1a2a3a4a5a6a7a8a9aaab",
"plaintext": "Day one. Planted in the dark — let's see what you become. 🌱",
"payload": "0x01a0a1a2a3a4a5a6a7a8a9aaabced18fcacb8a9ba999e6d1a9667db881e96ae446be0c9f92cab1bedede33eecc07ec01356fb921be2e593939a96a90aeaa4beb8bd5dd8e4fb68c753a6176c0ce6a02cab8d23c639844952a2a32db837e",
"contentHash": "0x55f71a3d186225ba6dafa99534d936f130a6ff972ed6d1b03801defe36f58c7a"
},
"publicEntry": {
"description": "Plaintext public entry → 0x00 ‖ UTF8(text) (no key, no encryption)",
"plaintext": "A public note anyone can read.",
"payload": "0x0041207075626c6963206e6f746520616e796f6e652063616e20726561642e",
"contentHash": "0x00a8f6044f6757a0fa5d433f317503f1126da9cc0111eba4c5146157dd4ff9a5"
},
"keyWrap": {
"description": "Hybrid (X25519 + ML-KEM-768) wrap of A's eraKey to B → 0x01 ‖ eph_pub(32) ‖ ct_kem(1088) ‖ wn(12) ‖ wct(32) ‖ wtag(16)",
"recipientPubKey": "0x0192ccc54d8600c7f19cf6e3364673d0e8e0cb78895b17599d1cf9edd0c25c453badd23581526d80911ae78b9f5df0521d0590046a73af152701cab464b30a0d91b4e0f778cf726cdffc73f6d1599a39732945b21c74648cfb28c5e62de25ac2fb83545ff3736a5068ca632a7d28820ce14c8ad1ae6b78011af55533713d74a9b9bc68af113076791c791c0076abb4217c218881e5b0cda69896765f00b131b5e22f5b33321c2b7057b85ce10266206b372758b987fc01dfc1770cdcbaf7b4944ea4458a2a615a3a842ab9983c830bdf198481437adc727a378abe56824338c3c72ea9a87fda3addfc583d6380b0b8354603647fd9215b2aa746b64659a82dee4a8dd91b0d56581424da22e2b18ff9382fef652356ca1203c7799a3206e53671f8b90df6823cf2a46dffa0aa8af6c4ffe67406abaaa8495ffcd1680b28248c92501ee370cb9bb69c9915ad7c6cf7733d1e14c95c11187fa69208497a61b5c0ea0c8d778a9362b1bcd7141fb18a3055ba8b26fc88c104240a467b8866a099d090accbb4c2a279b04b1f7201123f63ab10a0c0bc029f6308722b96761b0c079e6435fe0bc5c3226f23100ee8f6b620d21d2b8c0882c56f38001862b05a4686be627279bb997c3bd96ec2f042db60bf7fc427e7511454d49c60d23173dcc375a75bdf8397f1e24df4f28ea35ca840a2a7b0240b42c00114a600ffb00ebd2b204f148bbb31549459684fbc5ef0ca638d80b581027ff5ca9d4d66cb1467507dd53ecae303dae3ce244511c166b1131ba2129c8a50aa20b0284553fbc1acd47e9279052c36478397bb51fb01d52a27c0a3a88df728c709cbcd79b6fecc818d5bb9303a666986c4e7831f1be2a673b91b6e345c375ab7e1837abe25946543948c4233b926628856b1013093143431165b889cec63132946248b7fa54473682b9e7979632527879ec8b686bbc7381b48e7c3bccf2c74af067d86fb302a02cd3001063ba146bcfccb45272e7fc8715d8b5969f7bd5f4901f17ac611742e8159002299bed4db830e902a817a3cd3051ea38a6a08394593783eb580586e3b468bc51b47e712be1c36621908330c59cc3158045739efb787d3cabb56944ec7d82484ac9d992b44451c66ba7542eff11b0c8cbf72d95394755f33f8bdf6b79dfa27aa7e567fc7f93e4ba6cfc47a248c8aa2bf667f0b42b9f7d08acb0cadb914417c225290150c307c357707bf6b2880ebac9561604a33f782ebb4872802834b47abeed192b1290edf2b1c0de6618728473f6b9cb5eb4ffff168371935e37cbb2143998dea8347f05f264a20b7c576778635364cb03c7a86b8a95dc19083dd715111ba0ff8c7c39b3948811c09580bafa332af1354a857e85694359140fa1eaf3224ef65adc42109eef4a7f099c5521c1491b991c075429ae44ca569378cb33caa463afa117515562dedd2c2e782a4c08306e313460df251998bc1656883fe3852b9c2af7b1a3cc6d3032a8ab6956a42c2c6431172686b4337551cad2540af1df93560f74e211804d4a5ce64046f5d24857101c8a25c121724c6a3332034645d230a21303252831c3855a590586a2a22eb2519460b0e885f925b3dfca7cb023963084c3284b2a45a57341f4a1d8a6a69b6ba0d908a138d83942cfa1003fa745ac72d3f1bac5724bfa4fcee88f83d8cc089a44ebbd2d9813c68e11a129a3cc3683156ef152b7bf9ec",
"ephPriv": "0x3333333333333333333333333333333333333333333333333333333333333333",
"kemMsg": "0x4444444444444444444444444444444444444444444444444444444444444444",
"wn": "0xb0b1b2b3b4b5b6b7b8b9babb",
"wrappedEraKey": "0xe3c8fbf51f290cc62603ab8fef847a08cd172fd9cd57179435ba9671ab5a0c58",
"blob": "0x017b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14f046e419c8ce8e95f1cd51f6ff1acb9e1b20aa1dcf75831c3c91fa60318fb4ddfb1f9ab7f9aae04b930d0afeffe5a7921b06bdbe647322cd3867b56f09a8cbb08cf8f1a3295c4299920e35d5499c5344b6deea1fd8f2671607b8868a6822e72a5418703e033dc5da0776a3e96a967bb7b256e67d99737a7b8ea6cc0eb2d6fa3e216c3f3f7d538017221a2c1fe6b5d98fe974c5401a122d02dc9d36cdfa1a96efbaa0c9457aa66979684b604d49cd0d469ce1d42859447deb183ad38c317d634de17b3958384527ec83d3d025eab183906d963ece7e511fb0f3d48567918800f56868d1c9a19b687dda7f862e3a967f6feeede6d2dac134e5d7263b35fdd22923d9a0eab18443d6e5b5978094892c40942483bb55ce9af12dfcd4b6af222694bdcb5dc8a6532aec93fe4305794a6fd47e60c7216459cf30b4cf26a17c659fe0deb6a41eda7558ca42f0b05805b48f4551b34a667fc5ee4bb53cc66b15c24813a15cc1a9cdbcbd580eea3507140a23466f97dd1ebbf1ebc17e0947ec68d17d85a6b5a18e580a533238f517093d1415de2ebf17e2546b99ce3bf6c04f4d3736dc40e5325165ead40eda8b353cfff45081b85d5ba245d81e222c28abc9a31a19898ddd87fb6349a91d67c7f233a7c27f8a1d28e253db1408ddfb2f44fda3db9769ccac1915fd87efd2dd1daf021a401c6d87dd369d7605f35d184bfb794f6e165eeafd2822e2d3998ad93df92fdf40183b2a0dfc343d36f84167b387bb6380b24f8cdcd820d2deb9a56bbaa5344a80881b139c89560d921aa499bc505a77ce7aab31d51f1c55c402e765582bf9f8715584e67b66d96e1cba9d25d24efdf0120052f6c9d4df9c518923e9654f7fbe3a20837ec7961e1db6fa78bef9ec3e177cb385ee84a09663017eaf0b2c1b885047fdddae65f6e41f22430de0cf9723f54956428d0d85a86cec6bc077b80d1db247652d40c67f7bcf58734f141d1d35af96fcfbbd025960ed8895eb3297ff7dd385c8eb710418e1cefa442ab872a2be02b1db5fb52ef99061793863b8f8bd2cbd47cea2ff1d03720de4acb01045622fdd52788056ed4fcaace1e70e8074961584fee328404b5d2c0be2ffba93cf1a43be04cd6bb742447bd7054a66146c25ba8b4371034979eb1c3d021f3d57854a9a8bb37b8de28b943c3817f945fe52b3f5d6321c50e7aae7c0b1affd591109c358ba5ed299efe9832d763be93b37f076b8416249a3018bafebfa2e7c1a77b3a8ce1d7a7704507624419bd9e1f709aae501290e7a819c26ad1d19a61f4bcfdc552241e0afa84130780de070f76f6b8c583891ee0e2eafb784031f9fe7c4afb71eaddcf1616918bcdb26a82b4c4b99da62e5f7dff95da43a409d226268eb3aceb951d8d06f42c01fea5ec7e59b8ec26b80a9f44796b053254aaed7b8a0506932bc2d1b7102cb35b71f71bb0bb56727d0f72052c86f69b234750f9ede59be7222c5f8cbdc5d79dc9301b9ae61366c2e8add8b7af4b1113c73975a8f15c86b433d83892216b2d0acb0b1b2b3b4b5b6b7b8b9babbbeb037d5ca6e8b227461bf573bdee9efde925ced5447fea8b2c14255f0bb595bde639ee24507ffb831d196597b420dd9"
}
}
A living on-chain diary you grow as a plant. Write to water it; `neglect wilts it; every diary is` mortal. Fully on-chain, eternal`data:application/json;base64,A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``r{"name":"Seedling #`h - Revealing Soon","description"`3","attributes":[{"trait_type":"Status","value":"Revealing Soon"}` a ,V[o","attributes":[`{"trait_type":"Species","value":`{"trait_type":"Rarity","value":"`{"trait_type":"Status","value":"`{"trait_type":"Entries","value":`{"trait_type":"Watered Weeks","v`A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``<!doctype html><html><head><meta` <meta name="viewport" content="w`Qidth=device-width,initial-scale=`q<style>html,body{margin:0;height`:100%;background:#f3efe6}</style`</script><script>SeedlingRendere)</script></body></html>RlGolden Wattle`RoBird of Paradise`RlChilean Guava`RmJapanese Maple`RsAntarctic Hair Grass``ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` data:image/svg+xml;base64,<svg xmlns="http://www.w3.org/20` 00/svg" viewBox="0 0 600 600"><rect width="600" height="600" f`^<rect x="30" y="30" width="540" `height="540" fill="none" stroke=`"#3a4a3a" stroke-width="2"/><text x="60" y="110" font-family`="Georgia,serif" font-size="40" a<text x="60" y="160" font-family`="Georgia,serif" font-size="22" `'<text x="60" y="520" font-family`="monospace" font-size="20" fill`'<text x="60" y="550" font-family`="monospace" font-size="20" fill`'="#5a6a5a">watered weeks