Skip to content
This repository has been archived by the owner on Apr 21, 2023. It is now read-only.

Latest commit

 

History

History
629 lines (470 loc) · 26.8 KB

kid0003.md

File metadata and controls

629 lines (470 loc) · 26.8 KB
tags email
KERI

KID0003 Event Serialization

hackmd-github-sync-badge

Introduction

(from WhitePaper Version 2.59:)

In KERI, complete key events need to be serialized in order to create cryptographic signatures of each event. Complete events also need to be serialized in order to create cryptographic digests of each event. Complete events are propagated over the network in signed serialized form. Complete events also need to be de-serialized after network propagation such that the resultant de-serialization preserves the semantic structure of the data elements in the event. Because the cryptographic material in the events may be of variable length, a fixed field length serialization is not a viable approach. Consequently KERI must support variable length field serialization. Moreover serialization encodings that support arbitrary nested self-describing data structures provide future proofing to event composition. Notably, JSON, CBOR, and MsgPack are well known broadly supported serialization encodings that support arbitrary nested self-describing data structures. JSON is an ASCII text encoding that is human readable, albeit somewhat verbose, that is commonly used in web applications. On the other hand both CBOR and MsgPack are binary encodings that are much more compact. Supporting all three encodings (and possibly others) would satisfy a wide range of application, transport, and resource constaints. Importantly, because only the controller of the associated identifier prefix may compose KERI events, the ordering of elements in the event serialization may be determined solely by that controller. Other entities may sign the event serialization provided by the controller but do not need to provide a serialization of their own.

In summary, the necessary constraint for complete event serialization is support for arbitrary data structures with variable length fields that are serializable and de-serializable in multiple formats. Reproducible ordering is not a necessary constraint.

(For more rationale on serialization approach and JSON stringification, see Commentary).

Event Serialization Algorthm

The ordering of elements in each mapping in each event element is specified (see below). This includes nested elements. Serialization using the standard JSON, CBOR, and MsgPack libraries follow a recursive depth first traversal of nested elements. The ordered serialization depends on order preserving fields in the associated mappings. (See the section below on order preserving stringify for JSON)

Complete events are serialized/de-serialized from/to ordered mappings using the JSON, CBOR or MsgPack functions for serializing/deserializing. Each programming language may name the functions differently for serialization/de-serialization. In JavaScript they are JSON.stringify/JSON.parse. In Python they are json.dumps/json.loads. Likewise the CBOR and MsgPack libraries in each language may use different names for these functions.

The JSON serialization is without white space.

Event-Ordered Element Sets

As of this writing KERI has 5 different types of key events with corresponding message dictionaries and unique message types or ilks, these are:

Inception Event Rotation Event Interaction Event Delegated Inception Event Delegated Rotation Event

In addition to the key event messages KERI has several other messages such as receipt and notification messages each with corresponding message dictionaries and unique message types or ilks, these are:

Event Receipt Message, Nontransferable Signer Event Receipt Message, Transferable Signer Key State Notification Message with four variants

Each event may have slightly different sets of elements. The events are expressed as an ordered mapping of (label, value) pairs. The values may include lists (arrays) of values or (label, value) pairs or nested ordered mappings of (label, value) pairs.

An Interaction event in a delegated key event stream has the identical element set of an interaction event in a non-delegated key event stream. As a result the ordering from the non-delegated interaction event may be applied to the delegated interaction event.

In the following specifications the element sets are expressed in JSON format. Both MsgPack and CBOR natively support JSON compatible field element values for the various data types as well as ascii field element labels. As a result a JSON constrained representation is sufficient to ensure interoperabilty.

Element Labels

Label Description Notes
v Version String
i Identifier Prefix
s Sequence Number
t Message Type
te Last received Event Message Type in Key State Notice
d Event Digest (Seal or Receipt or Key State Notice)
p Prior Event Digest
kt Keys Signing Threshold
k List of Signing Keys (ordered key set)
n Next Key Set Commitment
wt Witnessing Threshold
w List of Witnesses (ordered witness set)
wr List of Witnesses to Remove (ordered witness set)
wa List of Witnesses to Add (ordered witness set)
c List of Configuration Traits/Modes
a List of Anchors (seals)
da Delegator Anchor Seal in Delegated Event (Location Seal)
di Delegator Identifier Prefix in Key State
rd Merkle Tree Root Digest
ee Last Establishment Event Map in Key State
vn Version Number ("major.minor") exact scheme tbd

A label may have different values in different contexts but not different value types.

Usages of Labels - Seals

Seals are mappings. There are 4 types of seals and one event reference that functions as a seal. The event reference is the last establishment event in the key state message. So I include it here for completeness.

Digest Merkle Root Digest Event Digest Event location Digest Last Event

Label Description Notes
i identifier prefix of KEL for event
s sequence number of event
t message type of event
d digest depends on context
p prior event digest depends on context
rd merkle tree root digest

Digest Seal has only one element

{
    "d": "Eabcde..."
}

Merkle Tree Root Digest Seal has only one element (hence is ambiguous wrt to digest seal

{
    "rd": "Eabcde..."
}

Event Seal has 3 elements

{
    "i": "Ebietyi....",
    "s": "3",
    "d": "Eabcde..."
}

Event location Seal has 4 elements

{
    "i": "Ebietyi....",
    "s": "3",
    "t":  "ixn",
    "p": "Eabcde..."
}

Last Received Event Reference in KeyState has 3 elements

{
   "s": "2",
   "t": "ixn",
   "d": "E4u8kd..."
}

Last Establishment Event Reference in KeyState has 2 elements

{
   "s": "2",
   "d": "E4u8kd..."
}

Usage of Labels - Messages

Inception Event

{
    "v" : "KERI10JSON00011c_",
    "i" : "EZAoTNZH3ULvaU6Z-i0d8JJR2nmwyYAfSVPzhzS6b5CM",
    "s" : "0",
    "t" :  "icp",
    "kt":  "1",
    "k" :  ["DaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"],
    "n" :  "EZ-i0d8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CM",
    "wt":  "1",
    "w" : ["DTNZH3ULvaU6JR2nmwyYAfSVPzhzS6bZ-i0d8JZAo5CM"],
    "c" :  ["EO"]
}

Rotation Event (also delegating rotation)

{
    "v" : "KERI10JSON00011c_",
    "i" :  "EZAoTNZH3ULvaU6Z-i0d8JJR2nmwyYAfSVPzhzS6b5CM",
    "s" : "1",
    "t" :  "rot",
    "p" : "EULvaU6JR2nmwyZ-i0d8JZAoTNZH3YAfSVPzhzS6b5CM",
    "kt" :  "1",
    "k" :  ["DaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"],
    "n" :  "EYAfSVPzhzZ-i0d8JZAoTNZH3ULvaU6JR2nmwyS6b5CM",
    "wt":  "1",
    "wa":  ["DTNZH3ULvaU6JR2nmwyYAfSVPzhzS6bZ-i0d8JZAo5CM"],
    "wr":   ["DH3ULvaU6JR2nmwyYAfSVPzhzS6bZ-i0d8TNZJZAo5CM"],
    "a" :
            [
               {
                   "i": "EJJR2nmwyYAfSVPzhzS6b5CMZAoTNZH3ULvaU6Z-i0d8",
                   "s": "0",
                   "d": "ELvaU6Z-i0d8JJR2nmwyYAZAoTNZH3UfSVPzhzS6b5CM"
               }
            ]
}

Interaction Event (Also delegating Interaction)

{
    "v" : "KERI10JSON00011c_",
    "i" :  "EZAoTNZH3ULvaU6Z-i0d8JJR2nmwyYAfSVPzhzS6b5CM",
    "s" : "2",
    "t" :  "isn",
    "p" : "EULvaU6JR2nmwyZ-i0d8JZAoTNZH3YAfSVPzhzS6b5CM",
    "a" :
            [
               {
                   "i": "EJJR2nmwyYAfSVPzhzS6b5CMZAoTNZH3ULvaU6Z-i0d8",
                   "s": "1",
                   "d": "ELvaU6Z-i0d8JJR2nmwyYAZAoTNZH3UfSVPzhzS6b5CM"
               }
            ]
}

Delegated Inception Event

{
    "v" : "KERI10JSON00011c_",
    "i" :  "EJJR2nmwyYAfSVPzhzS6b5CMZAoTNZH3ULvaU6Z-i0d8",
    "s" : "0",
    "t" :  "dip",
    "kt":  "1",
    "k" :  ["DaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"],
    "n" :  "EZ-i0d8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CM",
    "wt":  "1",
    "w" : ["DTNZH3ULvaU6JR2nmwyYAfSVPzhzS6bZ-i0d8JZAo5CM"],
    "c" :  ["DND"],
    "da" :
           {
             "i":  "EZAoTNZH3ULvaU6Z-i0d8JJR2nmwyYAfSVPzhzS6b5CM",
             "s": "1",
             "t": "rot",
             "p": "E8JZAoTNZH3ULZ-i0dvaU6JR2nmwyYAfSVPzhzS6b5CM"
           }
}

Delegated Rotation Event

{
    "v" : "KERI10JSON00011c_",
    "i" :  "EZAoTNZH3ULvaU6Z-i0d8JJR2nmwyYAfSVPzhzS6b5CM",
    "s" : "1",
    "t" :  "drt",
    "p" : "EULvaU6JR2nmwyZ-i0d8JZAoTNZH3YAfSVPzhzS6b5CM",
    "kt" :  "1",
    "k"  :  ["DaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"],
    "n"  :  "EYAfSVPzhzZ-i0d8JZAoTNZH3ULvaU6JR2nmwyS6b5CM",
    "wt":  "1",
    "wa":  ["DTNZH3ULvaU6JR2nmwyYAfSVPzhzS6bZ-i0d8JZAo5CM"],
    "wr":   ["DH3ULvaU6JR2nmwyYAfSVPzhzS6bZ-i0d8TNZJZAo5CM"],
    "a" : [ ],
    "da" :
           {
             "i":  "EZAoTNZH3ULvaU6Z-i0d8JJR2nmwyYAfSVPzhzS6b5CM",
             "s": "1",
             "t": "isn",
             "p": "E8JZAoTNZH3ULZ-i0dvaU6JR2nmwyYAfSVPzhzS6b5CM"
           }
}

Non-Transferable Prefix Signer Receipt

{
  "v"   : "KERI10JSON00011c_",
  "i"  : "AaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
  "s"   : "1",
  "t"  : "rct",
  "d"  : "DZ-i0d8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CM"
}

Transferable Prefix Signer Receipt

{
  "v"   : "KERI10JSON00011c_",
  "i"  : "AaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
  "s"   : "1",
  "t"  : "vrc",
  "d"  : "DZ-i0d8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CM",
  "a" :
          {
             "i"  : "AYAfSVPzhzS6b5CMaU6JR2nmwyZ-i0d8JZAoTNZH3ULv",
             "s":  "4",
             "d" : "DZ-i0d8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CM",
           }
}

Key State Notice Messages

While the actual Key State only needs the version number, that message itself needs the full version string so that it can be parsed when sent over the wire using low level TCP etc to support all the serializations. A different version of the message could be used with a higher level transport such as JSON only ReST. In that case then the "v" could be replaced with "vn" = "1.0" for example.

The message type is ksn for Key State Notice message. Delegated key state notification messages have a non-empty di field for the delegator's fully qualified identifier prefix.

The ee block or map means last received establishment event in key state. It includes the witness remove, wr, and witness add, wa lists from that establishment event. This is to enable promulgation and confirmation that any rotated out witnesses also witness the event that rotates them out from being a witness.

Each key state notification message is signed by the entity providing the key state. The entity providing the key state may not necessarily be the controlling entity of the associate KEL for that key state. Essentially the signature on a key state notification message acts as an endorsement of that key state message by the provider. It is signifying that the endorsing provider has verified a given KEL and the result of its verification is the provided key state. Because the key state message is thusly signed any duplicity on the part of the endorsing provider (signer) now becomes detectable and provable. As a result duplicitous endorsers (signers) may therefore not be trusted by any validator that detects such duplicity.

When the endorser has a nontransferable identifier then the attached signatures consist of a receipt couple count code plus one or more receipt couples. A receipt couple consists of a fully qualified non-transferable identifier followed by a fully qualified non-indexed signature. The full attachment may also include prelude pipeline count codes. Multiple receipt couples may occur if a given key state came from a cluster of watchers/or witnesses where each mutually consistent individual key state messages from were merged into one message with all their receipts attached in a group.

When the endorser has a transferable identifier then the attached signatures consist of a transferable indexed signature group count code plus one or more complex indexed signature groups. The full attachments may also include prelude pipeline count codes. Each transferable indexed signature group consists of a triple of pre+snu+dig corresponding to the (i, s, d) of the establishment event from the endorser that was authoritative at the time of the endorsement. The key list of this establishment event provides the set of public keys for the attached indexed signatures. This triple is followed by a controller indexed signature count code and one or more indexed signatures corresponding to the keys from the establishment event of the preceding triple. The index in each signature corresponds to the offset of the public key in the key list.

Key State Notice (Non-Delegated)

{
  "v": "KERI10JSON00011c_",
  "i": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
  "s": "2",
  "t": "ksn",
  "d": "EAoTNZH3ULvaU6JR2nmwyYAfSVPzhzZ-i0d8JZS6b5CM",
  "te": "rot",
  "kt": "1",
  "k": ["DaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"],
  "n": "EZ-i0d8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CM",
  "wt": "1",
  "w": ["DnmwyYAfSVPzhzS6b5CMZ-i0d8JZAoTNZH3ULvaU6JR2"],
  "c": ["eo"],
  "ee":
    {
      "s":  "1",
      "d":  "EAoTNZH3ULvaU6JR2nmwyYAfSVPzhzZ-i0d8JZS6b5CM",
      "wr": ["Dd8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CMZ-i0"],
      "wa": ["DnmwyYAfSVPzhzS6b5CMZ-i0d8JZAoTNZH3ULvaU6JR2"]
    },
  "di": ""
}

Key State Notice (Delegated)

{
  "v": "KERI10JSON00011c_",
  "i": "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM",
  "s": "2",
  "t": "ksn",
  "d": "EAoTNZH3ULvaU6JR2nmwyYAfSVPzhzZ-i0d8JZS6b5CM",
  "te": "rot",
  "kt": "1",
  "k": ["DaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM"],
  "n": "EZ-i0d8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CM",
  "wt": "1",
  "w": ["DnmwyYAfSVPzhzS6b5CMZ-i0d8JZAoTNZH3ULvaU6JR2"],
  "c": ["eo"],
  "ee":
    {
      "s":  "1",
      "d":  "EAoTNZH3ULvaU6JR2nmwyYAfSVPzhzZ-i0d8JZS6b5CM",
      "wr": ["Dd8JZAoTNZH3ULvaU6JR2nmwyYAfSVPzhzS6b5CMZ-i0"],
      "wa": ["DnmwyYAfSVPzhzS6b5CMZ-i0d8JZAoTNZH3ULvaU6JR2"]
    },
  "di": "EJZAoTNZH3ULvYAfSVPzhzS6b5CMaU6JR2nmwyZ-i0d8"
}

Usage of Labels - Configurations and System Metadata

Configuration Traits/Modes

The "c" element in inception and delegated inception events is a list strings specifying optional configuration data/traits/modes/options . The presence of a string in this list indicates a configuration option for the Key Events. The string values are provided in the following table. The order in which the string values appear in the trait list is normative. They must appear when present in the order in the following table. To clarify, when present EO must be first. When both EO and DND are present then EO must precede DND. If EO is not present then DND may be first in the list. Subsequent additions to the following table must follow that same precedence ordering rule. The reason for the rule is to ensure consistency (reproducability) in the key state notification message.

Option Description Notes
EO Establishment Only Only establishment events are allowed for this identifier prefix
DND Do Not Delegate This identifier prefix does not allow delegations to other identifier prefixes

Version String

Version string enables discovery of encoding when parsing serialized packet.

Parts: KERI version kind size terminal

KERI = KERI event packet identifier
version = major minor = hex characters = 10 for 1.0
kind  = encoding one of CBOR, JSOM, MGPK
size = 6 character hex string length of serialized event
       max size 16777215 bytes in version 1.0
terminal is "_" underscore for future proofing

version tuple = (1, 0) = (major, minor)

major increments with backward breaking changes minor increments with backward compatible changes

Serialize event with 0000 size value in version string.

Terminal characters enables detectable parsable change in length of version string in the future

Embedded version string with size obviates the needed for separate header when using headerless transports (not http). Because version string is embedded in event it is included in signed portion so is protected by authentication of signature. An attacker may not change the size of the event without breaking the signature.

Serialize event with zero'd size in version string

KERI10CBOR000000_
KERI10JSON000000_
KERI10MGPK000000_

Then replace the 000000 size field inside the version string of the serialized event with lowercase hex of actual size of serialized event.

KERI10CBOR000115_
KERI10JSON00011c_
KERI10MGPK000115_

Then sign the encoded event that now has the correct size in its version string.

Next Signing Config Digest for Future Event Labels

The KERI pre-rotation scheme includes a commitment to the next signing configuration. This includes the next threshold and the next set of signing keys. This committment is made by creating a digest of each of the elements in this committment set and then combining them with a bitwize XOR (exclusive OR). The labels of the elements that are included for the next set are as follows:

[
  "kt",  //Signing threshold, key element threshold
  "k",  //List (array) of signing keys
]

The next signing config has two primary elements, kt and k. The combination starts with the kt as the first element. The k element is then combined. But because the keys element is a list, each key from the list must be combined in order of appearance with the combination of the prior elements. The "kt" element may also be a list or list of lists when using a fractionally weighted threshold.

The combination method is to first take a digest of each element and then combine the elements with an XOR. Finally the combined result is converted to a fully qualified Base64 representation with the derivation code for the type of digest used. All elements must use the same digest.

The kt element is converted to a string before digesting. For an integer kt, the string is a hex representation of the integer with no leading zeros.

Each of the keys in k is first converted to fully qualified Base64 format which is the normal format for the keys in key events. The digest is in this fully-qualified Base64 format. The digests, kt plus each of the keys, are combined in raw binary mode using a bitwise XOR. The combined digests are then converted to fully qualified Base64 and this is the format used in the key event for the value of the n element.

Example computation

The following example shows values.

See element labels for field name abbreviations.

kt = 2
k = ['BrHLayDN-mXKv62DAjFLX1_Y5yEUe0vA9YPe_ihiKYHE',
        'BujP_71bmWFVcvFmkE9uS8BTZ54GIstZ20nj_UloF8Rk',
        'B8T4xkb8En6o0Uo5ZImco1_08gT5zcYnXzizUPVNzicw']

# convert keys element threshold to hex string with no leading zeros

kt = '2'

# kt digest
# fully qualified
sithdig = 'EgT6bcpFB5_OFr6Ci0N8-bDeJ5Cf_5K7vVmpWW8jy_j0'

# raw binary unqualified in string escaped form
sithdig == b"\x81>\x9br\x91A\xe7\xf3\x85\xaf\xa0\xa2\xd0\xdf>l7\x89\xe4'\xff\xe4\xae\xefVjV[\xc8\xf2\xfe="

# convert keys to digests
# fully qualified Base64 digests
keydigs == ['EmB26yMzroICh-opKNdkYyP000kwevU18WQI95JaJDjY',
         'EO4CXp8gs0yJg1fFhJLs5hH6neqJwhFEY7vrJEdPe87I',
         'ELWWZEyBpjrfM1UU0n31KIyIXllrCoLEOI5UHD9x7WxI']

# raw binary (unqualifed) digests in string escaped form
keydigs =[b'\x98\x1d\xba\xc8\xcc\xeb\xa0\x80\xa1\xfa\x8aJ5\xd9\x18\xc8\xfd4\xd2L\x1e\xbdM|Y\x02=\xe4\x96\x89\x0e6',
                       b';\x80\x97\xa7\xc8,\xd3"`\xd5\xf1a$\xbb9\x84~\xa7z\xa2p\x84Q\x18\xee\xfa\xc9\x11\xd3\xde\xf3\xb2',
                       b'-e\x99\x13 i\x8e\xb7\xcc\xd5E4\x9f}J#"\x17\x96Z\xc2\xa0\xb1\x0e#\x95\x07\x0f\xdc{[\x12']

# now combine with XOR the raw versions of all four digests, sith plus three keys. In python must first convert to integers in order to use XOR = ^

raw = sithdig ^ keydig[0] ^ keydig[1] ^ keydig[2]

# now convert raw to fully qualified with derivation code for the digest algorithm
# in this case Blake3_256

n = 'ED8YvDrXvGuaIVZ69XsBVA5YN2pNTfQOFwgeloVHeWKs'

Note: The keripy implementation is in keri.core.coring.py in the Nextor object class ._derive method. Test in test_nexter() function in the tests.core.test_coring.py .

The motivation for this new revised method is to enable multiple-signature controlled prefixes where the controllers of each public key are distributed. In this case, in order to create the n commitment, the public keys would otherwise have to be transmitted outside of the key creation infrastructure. This would expose them to a post-quantum attack on the public keys themselves. With the new method, only the digest of the public key need be transmitted outside the key creation infrastructure from each of the controllers. The digest of the public key is post quantum proof and therefore is not vulnerable to a post quantum attack.

Prefix Derivation Data from Inception Event

Prefixes are derived from Inception Event Data using the full event serialization, with the Prefix in the serialized event replaced with a string of # characters of the correct length according to the desired Prefix derivation method. This allows future proofing in that other elements included in inception event do not break derivation.

Delegated Prefix Derivation Data from Delegated Inception Event

Delegated Prefixes are derived from Delegated Inception Event Data using the full event serialization, with the Prefix in the serialized event replaced with a string of # characters of the correct length according to the desired Prefix derivation method. This allows future proofing in that other elements included in inception event do not break derivation.

Delegating Seal Digest Data

digest of complete serialized delegated event

JavaScript Ordered Object Property Serialization (Stringify)

The following code snippet shows how to enforce object property creation order preserving serialization using JSON.stringify for ES6-ES10. Not needed for ES11 or later.

Code Snippet

"use strict"

// Spec http://www.ecma-international.org/ecma-262/6.0/#sec-json.stringify
const replacer = (key, value) =>
  value instanceof Object && !(value instanceof Array) ?
    Reflect.ownKeys(value)
    .reduce((ordered, key) => {
      ordered[key] = value[key];
      return ordered
    }, {}) :
    value;

// Usage

// JSON.stringify({c: 1, a: { d: 0, c: 1, e: {a: 0, 1: 4}}}, replacer);


var main = function()
{
    console.log("Running Main.");

    let x = { zip: 3, apple: 1, bulk: 2, _dog: 4 };
    let y = JSON.stringify(x);
    console.log(y);
    console.log(Object.keys(x));
    console.log(Object.getOwnPropertyNames(x));
    console.log(Reflect.ownKeys(x));
    console.log(replacer);
    let w  = {w: 1, a: x, c: {e: 2, b: 3}};
    let z = JSON.stringify(x, replacer);
    console.log(z);
    let v = JSON.stringify(w, replacer);
    console.log(v);

}

if (require.main === module)
{
    main();
}


//nodejs % node play.js
//Running Main.
//{"zip":3,"apple":1,"bulk":2,"_dog":4}
//[ 'zip', 'apple', 'bulk', '_dog' ]
//[ 'zip', 'apple', 'bulk', '_dog' ]
//[ 'zip', 'apple', 'bulk', '_dog' ]
//[Function: replacer]
//{"zip":3,"apple":1,"bulk":2,"_dog":4}
//{"w":1,"a":{"zip":3,"apple":1,"bulk":2,"_dog":4},"c":{"e":2,"b":3}}

Background

We may impose ordered serialization with the JSON.stringify method by using the replacer option and the [[ownPropertyKeys]] internal method added in Javascript ES6 exposed as Reflect.ownKeys(). This method is provided in JavaScript ES6 (2015). [[ownPropertyKeys]] uses the property creation order. At the time of ES6, JSON.stringify was not required to use the [[ownPropertyKeys]] iteration order (ie property creation order). This appears to have been fixed in later versions of ES. (See the discussion below). As of this writing nodejs v14.2.0 and the latest versions of Chrome, Safari, and Firefox all appear to preserve property creation order in JSON.stringify() without having to use  Reflect.ownKeys(). This means that we can depend on and use that ordering in the specification. For earlier versions but still with support ES6, a custom replacer function for JSON.stringify(x, replacer) will ensure that property creation order is used. Earlier versions that do not support ES6 may still work with an ES6 polyfill if that includes support for [[ownPropertyKeys]].

https://stackoverflow.com/questions/30076219/does-es6-introduce-a-well-defined-order-of-enumeration-for-object-properties/30919039

In ES6 the stringify order is not required to follow the ownPropertyKeys (property creation order) ordering but this will be fixed in ES 2020 == ES11. It appears that the Babel polyfill as of 2020 already supports ES2020 already.

https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-2-reflect/

Reflect.ownKeys ( target )

This has already been discussed a tiny bit in this article, you see Reflect.ownKeys implements [[OwnPropertyKeys]] which if you recall above is a combination of Object.getOwnPropertyNames and Object.getOwnPropertySymbols. This makes Reflect.ownKeys uniquely useful. Lets see shall we:

var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4,
};

assert.deepEqual(Object.getOwnPropertyNames(myObject), ['foo', 'bar']);
assert.deepEqual(Object.getOwnPropertySymbols(myObject), [Symbol.for('baz'), Symbol.for('bing')]);

// Without Reflect.ownKeys:
var keys = Object.getOwnPropertyNames(myObject).concat(Object.getOwnPropertySymbols(myObject));
assert.deepEqual(keys, ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

// With Reflect.ownKeys:
assert.deepEqual(Reflect.ownKeys(myObject), ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

(For more rationale on serialization approach and JSON stringification, see Commentary.

Stringify References