2023-05-11 14:25:29 +00:00
|
|
|
# nimbus-eth1
|
2024-02-20 03:07:38 +00:00
|
|
|
# Copyright (c) 2023-2024 Status Research & Development GmbH
|
2023-05-11 14:25:29 +00:00
|
|
|
# Licensed under either of
|
|
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0)
|
|
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
|
|
# http://opensource.org/licenses/MIT)
|
|
|
|
# at your option. This file may not be copied, modified, or distributed
|
|
|
|
# except according to those terms.
|
|
|
|
|
|
|
|
{.push raises: [].}
|
|
|
|
|
|
|
|
import
|
2024-06-22 20:33:37 +00:00
|
|
|
eth/common,
|
2023-09-15 15:23:53 +00:00
|
|
|
results,
|
2024-07-02 18:25:06 +00:00
|
|
|
stew/[arrayops, endians2],
|
2023-11-08 12:18:32 +00:00
|
|
|
./aristo_desc
|
2023-05-11 14:25:29 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
# Allocation-free version of the RLP integer encoding, returning the shortest
|
|
|
|
# big-endian representation - to decode, the length must be known / stored
|
|
|
|
# elsewhere
|
|
|
|
type
|
|
|
|
RlpBuf*[I] = object
|
|
|
|
buf*: array[sizeof(I), byte]
|
|
|
|
len*: byte
|
|
|
|
|
|
|
|
func significantBytesBE(val: openArray[byte]): byte =
|
|
|
|
for i in 0 ..< val.len:
|
|
|
|
if val[i] != 0:
|
|
|
|
return byte(val.len - i)
|
|
|
|
return 1
|
|
|
|
|
|
|
|
func blobify*(v: VertexID|uint64): RlpBuf[typeof(v)] =
|
|
|
|
let b = v.uint64.toBytesBE()
|
|
|
|
RlpBuf[typeof(v)](buf: b, len: significantBytesBE(b))
|
|
|
|
|
|
|
|
func blobify*(v: StUint): RlpBuf[typeof(v)] =
|
|
|
|
let b = v.toBytesBE()
|
|
|
|
RlpBuf[typeof(v)](buf: b, len: significantBytesBE(b))
|
|
|
|
|
|
|
|
template data*(v: RlpBuf): openArray[byte] =
|
|
|
|
let vv = v
|
|
|
|
vv.buf.toOpenArray(vv.buf.len - int(vv.len), vv.buf.high)
|
|
|
|
|
|
|
|
|
|
|
|
proc deblobify*[T: uint64|VertexID](data: openArray[byte], _: type T): Result[T,AristoError] =
|
|
|
|
if data.len < 1 or data.len > 8:
|
|
|
|
return err(DeblobPayloadTooShortInt64)
|
|
|
|
|
|
|
|
var tmp: array[8, byte]
|
|
|
|
discard tmp.toOpenArray(8 - data.len, 7).copyFrom(data)
|
|
|
|
|
|
|
|
ok T(uint64.fromBytesBE(tmp))
|
|
|
|
|
|
|
|
proc deblobify*(data: openArray[byte], _: type UInt256): Result[UInt256,AristoError] =
|
|
|
|
if data.len < 1 or data.len > 32:
|
|
|
|
return err(DeblobPayloadTooShortInt256)
|
|
|
|
|
|
|
|
ok UInt256.fromBytesBE(data)
|
|
|
|
|
2023-05-11 14:25:29 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
2023-07-05 20:27:48 +00:00
|
|
|
# Private helper
|
2023-05-11 14:25:29 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
proc load64(data: openArray[byte]; start: var int, len: int): Result[uint64,AristoError] =
|
|
|
|
if data.len < start + len:
|
2023-07-05 20:27:48 +00:00
|
|
|
return err(DeblobPayloadTooShortInt64)
|
2024-07-02 18:25:06 +00:00
|
|
|
|
|
|
|
let val = ?deblobify(data.toOpenArray(start, start + len - 1), uint64)
|
|
|
|
start += len
|
2023-07-05 20:27:48 +00:00
|
|
|
ok val
|
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
proc load256(data: openArray[byte]; start: var int, len: int): Result[UInt256,AristoError] =
|
|
|
|
if data.len < start + len:
|
2023-07-05 20:27:48 +00:00
|
|
|
return err(DeblobPayloadTooShortInt256)
|
2024-07-02 18:25:06 +00:00
|
|
|
let val = ?deblobify(data.toOpenArray(start, start + len - 1), UInt256)
|
|
|
|
start += len
|
2023-07-05 20:27:48 +00:00
|
|
|
ok val
|
|
|
|
|
2023-05-11 14:25:29 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
2023-09-15 15:23:53 +00:00
|
|
|
# Public functions
|
2023-05-11 14:25:29 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
2024-06-01 15:13:24 +00:00
|
|
|
proc blobifyTo*(pyl: PayloadRef, data: var Blob) =
|
2023-07-05 20:27:48 +00:00
|
|
|
if pyl.isNil:
|
|
|
|
return
|
|
|
|
case pyl.pType
|
|
|
|
of RawData:
|
2024-06-01 15:13:24 +00:00
|
|
|
data &= pyl.rawBlob
|
2024-07-02 18:25:06 +00:00
|
|
|
data &= [0x10.byte]
|
2023-07-05 20:27:48 +00:00
|
|
|
|
|
|
|
of AccountData:
|
2024-07-02 18:25:06 +00:00
|
|
|
# `lens` holds `len-1` since `mask` filters out the zero-length case (which
|
|
|
|
# allows saving 1 bit per length)
|
|
|
|
var lens: uint16
|
2023-07-05 20:27:48 +00:00
|
|
|
var mask: byte
|
|
|
|
if 0 < pyl.account.nonce:
|
|
|
|
mask = mask or 0x01
|
2024-07-02 18:25:06 +00:00
|
|
|
let tmp = pyl.account.nonce.blobify()
|
|
|
|
lens += tmp.len - 1 # 3 bits
|
|
|
|
data &= tmp.data()
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
if 0 < pyl.account.balance:
|
|
|
|
mask = mask or 0x02
|
|
|
|
let tmp = pyl.account.balance.blobify()
|
|
|
|
lens += uint16(tmp.len - 1) shl 3 # 5 bits
|
|
|
|
data &= tmp.data()
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-06-27 09:01:26 +00:00
|
|
|
if VertexID(0) < pyl.stoID:
|
2024-07-02 18:25:06 +00:00
|
|
|
mask = mask or 0x04
|
|
|
|
let tmp = pyl.stoID.blobify()
|
|
|
|
lens += uint16(tmp.len - 1) shl 8 # 3 bits
|
|
|
|
data &= tmp.data()
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
if pyl.account.codeHash != EMPTY_CODE_HASH:
|
|
|
|
mask = mask or 0x08
|
2024-06-01 15:13:24 +00:00
|
|
|
data &= pyl.account.codeHash.data
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
data &= lens.toBytesBE()
|
2024-06-01 15:13:24 +00:00
|
|
|
data &= [mask]
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-06-01 15:13:24 +00:00
|
|
|
proc blobifyTo*(vtx: VertexRef; data: var Blob): Result[void,AristoError] =
|
2023-06-30 22:22:33 +00:00
|
|
|
## This function serialises the vertex argument to a database record.
|
|
|
|
## Contrary to RLP based serialisation, these records aim to align on
|
|
|
|
## fixed byte boundaries.
|
2023-05-11 14:25:29 +00:00
|
|
|
## ::
|
|
|
|
## Branch:
|
2024-07-02 18:25:06 +00:00
|
|
|
## [VertexID, ...] -- list of up to 16 child vertices lookup keys
|
|
|
|
## uint64 -- lengths of each child vertex, each taking 4 bits
|
|
|
|
## 0x08 -- marker(8)
|
2023-05-11 14:25:29 +00:00
|
|
|
##
|
|
|
|
## Extension:
|
2024-07-02 18:25:06 +00:00
|
|
|
## VertexID -- child vertex lookup key
|
2023-05-11 14:25:29 +00:00
|
|
|
## Blob -- hex encoded partial path (at least one byte)
|
2023-08-21 18:18:06 +00:00
|
|
|
## 0x80 + xx -- marker(2) + pathSegmentLen(6)
|
2023-05-11 14:25:29 +00:00
|
|
|
##
|
|
|
|
## Leaf:
|
|
|
|
## Blob -- opaque leaf data payload (might be zero length)
|
|
|
|
## Blob -- hex encoded partial path (at least one byte)
|
2023-08-21 18:18:06 +00:00
|
|
|
## 0xc0 + yy -- marker(2) + partialPathLen(6)
|
2023-05-11 14:25:29 +00:00
|
|
|
##
|
|
|
|
## For a branch record, the bytes of the `access` array indicate the position
|
2023-06-12 18:16:03 +00:00
|
|
|
## of the Patricia Trie vertex reference. So the `vertexID` with index `n` has
|
2023-05-11 14:25:29 +00:00
|
|
|
## ::
|
|
|
|
## 8 * n * ((access shr (n * 4)) and 15)
|
|
|
|
##
|
2023-09-05 13:57:20 +00:00
|
|
|
if not vtx.isValid:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(BlobifyNilVertex)
|
2023-06-30 22:22:33 +00:00
|
|
|
case vtx.vType:
|
2023-05-11 14:25:29 +00:00
|
|
|
of Branch:
|
|
|
|
var
|
2024-07-02 18:25:06 +00:00
|
|
|
lens = 0u64
|
2024-06-01 15:13:24 +00:00
|
|
|
pos = data.len
|
2023-05-11 14:25:29 +00:00
|
|
|
for n in 0..15:
|
2023-06-30 22:22:33 +00:00
|
|
|
if vtx.bVid[n].isValid:
|
2024-07-02 18:25:06 +00:00
|
|
|
let tmp = vtx.bVid[n].blobify()
|
|
|
|
lens += uint64(tmp.len) shl (n * 4)
|
|
|
|
data &= tmp.data()
|
|
|
|
if data.len == pos:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(BlobifyBranchMissingRefs)
|
2024-07-02 18:25:06 +00:00
|
|
|
data &= lens.toBytesBE
|
2024-06-01 15:13:24 +00:00
|
|
|
data &= [0x08u8]
|
2023-05-11 14:25:29 +00:00
|
|
|
of Extension:
|
|
|
|
let
|
2024-06-22 20:33:37 +00:00
|
|
|
pSegm = vtx.ePfx.toHexPrefix(isleaf = false)
|
2023-05-11 14:25:29 +00:00
|
|
|
psLen = pSegm.len.byte
|
2024-02-20 03:07:38 +00:00
|
|
|
if psLen == 0 or 33 < psLen:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(BlobifyExtPathOverflow)
|
2023-06-30 22:22:33 +00:00
|
|
|
if not vtx.eVid.isValid:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(BlobifyExtMissingRefs)
|
2024-07-02 18:25:06 +00:00
|
|
|
data &= vtx.eVid.blobify().data()
|
2024-06-01 15:13:24 +00:00
|
|
|
data &= pSegm
|
|
|
|
data &= [0x80u8 or psLen]
|
2023-05-11 14:25:29 +00:00
|
|
|
of Leaf:
|
|
|
|
let
|
2024-06-22 20:33:37 +00:00
|
|
|
pSegm = vtx.lPfx.toHexPrefix(isleaf = true)
|
2023-05-11 14:25:29 +00:00
|
|
|
psLen = pSegm.len.byte
|
|
|
|
if psLen == 0 or 33 < psLen:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(BlobifyLeafPathOverflow)
|
2024-06-01 15:13:24 +00:00
|
|
|
vtx.lData.blobifyTo(data)
|
|
|
|
data &= pSegm
|
|
|
|
data &= [0xC0u8 or psLen]
|
2024-07-02 18:25:06 +00:00
|
|
|
|
2023-09-12 18:45:12 +00:00
|
|
|
ok()
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2023-06-30 22:22:33 +00:00
|
|
|
proc blobify*(vtx: VertexRef): Result[Blob, AristoError] =
|
2023-05-11 14:25:29 +00:00
|
|
|
## Variant of `blobify()`
|
2023-09-12 18:45:12 +00:00
|
|
|
var data: Blob
|
2024-06-01 15:13:24 +00:00
|
|
|
? vtx.blobifyTo data
|
2024-05-24 09:27:17 +00:00
|
|
|
ok(move(data))
|
2023-05-11 14:25:29 +00:00
|
|
|
|
2024-06-05 18:17:50 +00:00
|
|
|
proc blobifyTo*(lSst: SavedState; data: var Blob): Result[void,AristoError] =
|
2024-05-31 17:32:22 +00:00
|
|
|
## Serialise a last saved state record
|
2024-06-28 18:43:04 +00:00
|
|
|
data.add lSst.key.data
|
2024-06-03 20:10:35 +00:00
|
|
|
data.add lSst.serial.toBytesBE
|
|
|
|
data.add @[0x7fu8]
|
2024-06-05 18:17:50 +00:00
|
|
|
ok()
|
2024-05-31 17:32:22 +00:00
|
|
|
|
2024-06-05 18:17:50 +00:00
|
|
|
proc blobify*(lSst: SavedState): Result[Blob,AristoError] =
|
2024-05-31 17:32:22 +00:00
|
|
|
## Variant of `blobify()`
|
2024-06-05 18:17:50 +00:00
|
|
|
var data: Blob
|
|
|
|
? lSst.blobifyTo data
|
|
|
|
ok(move(data))
|
2024-05-31 17:32:22 +00:00
|
|
|
|
2023-07-05 20:27:48 +00:00
|
|
|
# -------------
|
2024-07-02 18:25:06 +00:00
|
|
|
proc deblobify(
|
2024-05-31 17:32:22 +00:00
|
|
|
data: openArray[byte];
|
2024-07-02 18:25:06 +00:00
|
|
|
T: type PayloadRef;
|
|
|
|
): Result[PayloadRef,AristoError] =
|
2023-07-05 20:27:48 +00:00
|
|
|
if data.len == 0:
|
2024-07-02 18:25:06 +00:00
|
|
|
return ok PayloadRef(pType: RawData)
|
2023-07-05 20:27:48 +00:00
|
|
|
|
|
|
|
let mask = data[^1]
|
2024-07-02 18:25:06 +00:00
|
|
|
if (mask and 0x10) > 0: # unstructured payload
|
|
|
|
return ok PayloadRef(pType: RawData, rawBlob: data[0 .. ^2])
|
2023-09-12 18:45:12 +00:00
|
|
|
|
2023-07-05 20:27:48 +00:00
|
|
|
var
|
|
|
|
pAcc = PayloadRef(pType: AccountData)
|
|
|
|
start = 0
|
2024-07-02 18:25:06 +00:00
|
|
|
lens = uint16.fromBytesBE(data.toOpenArray(data.len - 3, data.len - 2))
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
if (mask and 0x01) > 0:
|
|
|
|
let len = lens and 0b111
|
|
|
|
pAcc.account.nonce = ? load64(data, start, int(len + 1))
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
if (mask and 0x02) > 0:
|
|
|
|
let len = (lens shr 3) and 0b11111
|
|
|
|
pAcc.account.balance = ? load256(data, start, int(len + 1))
|
|
|
|
|
|
|
|
if (mask and 0x04) > 0:
|
|
|
|
let len = (lens shr 8) and 0b111
|
|
|
|
pAcc.stoID = VertexID(? load64(data, start, int(len + 1)))
|
|
|
|
|
|
|
|
if (mask and 0x08) > 0:
|
|
|
|
if data.len() < start + 32:
|
|
|
|
return err(DeblobCodeLenUnsupported)
|
|
|
|
discard pAcc.account.codeHash.data.copyFrom(data.toOpenArray(start, start + 31))
|
2023-07-05 20:27:48 +00:00
|
|
|
else:
|
2024-07-02 18:25:06 +00:00
|
|
|
pAcc.account.codeHash = EMPTY_CODE_HASH
|
2023-07-05 20:27:48 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
ok(pAcc)
|
2023-05-11 14:25:29 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
proc deblobify*(
|
2024-06-03 20:10:35 +00:00
|
|
|
record: openArray[byte];
|
2024-07-02 18:25:06 +00:00
|
|
|
T: type VertexRef;
|
|
|
|
): Result[T,AristoError] =
|
2023-05-11 14:25:29 +00:00
|
|
|
## De-serialise a data record encoded with `blobify()`. The second
|
|
|
|
## argument `vtx` can be `nil`.
|
|
|
|
if record.len < 3: # minimum `Leaf` record
|
2023-11-08 12:18:32 +00:00
|
|
|
return err(DeblobVtxTooShort)
|
2023-05-11 14:25:29 +00:00
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
ok case record[^1] shr 6:
|
2023-06-12 18:16:03 +00:00
|
|
|
of 0: # `Branch` vertex
|
2023-08-21 18:18:06 +00:00
|
|
|
if record[^1] != 0x08u8:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(DeblobUnknown)
|
2024-07-02 18:25:06 +00:00
|
|
|
if record.len < 11: # at least two edges
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(DeblobBranchTooShort)
|
2023-05-11 14:25:29 +00:00
|
|
|
let
|
2024-07-02 18:25:06 +00:00
|
|
|
aInx = record.len - 9
|
2023-05-11 14:25:29 +00:00
|
|
|
aIny = record.len - 2
|
|
|
|
var
|
|
|
|
offs = 0
|
2024-07-02 18:25:06 +00:00
|
|
|
lens = uint64.fromBytesBE record.toOpenArray(aInx, aIny) # bitmap
|
2023-05-11 14:25:29 +00:00
|
|
|
vtxList: array[16,VertexID]
|
2024-07-02 18:25:06 +00:00
|
|
|
n = 0
|
|
|
|
while lens != 0:
|
|
|
|
let len = lens and 0b1111
|
|
|
|
if len > 0:
|
|
|
|
vtxList[n] = VertexID(? load64(record, offs, int(len)))
|
|
|
|
inc n
|
|
|
|
lens = lens shr 4
|
|
|
|
|
2023-05-11 14:25:29 +00:00
|
|
|
# End `while`
|
2024-07-02 18:25:06 +00:00
|
|
|
VertexRef(
|
2023-05-11 14:25:29 +00:00
|
|
|
vType: Branch,
|
2023-05-14 17:43:01 +00:00
|
|
|
bVid: vtxList)
|
2023-05-11 14:25:29 +00:00
|
|
|
|
2023-06-12 18:16:03 +00:00
|
|
|
of 2: # `Extension` vertex
|
2023-05-11 14:25:29 +00:00
|
|
|
let
|
|
|
|
sLen = record[^1].int and 0x3f # length of path segment
|
2024-02-20 03:07:38 +00:00
|
|
|
rLen = record.len - 1 # `vertexID` + path segm
|
2024-07-02 18:25:06 +00:00
|
|
|
pLen = rLen - sLen # payload length
|
|
|
|
if rLen < sLen or pLen < 1:
|
|
|
|
return err(DeblobLeafSizeGarbled)
|
2024-06-22 20:33:37 +00:00
|
|
|
let (isLeaf, pathSegment) =
|
2024-07-02 18:25:06 +00:00
|
|
|
NibblesBuf.fromHexPrefix record.toOpenArray(pLen, rLen - 1)
|
2023-05-11 14:25:29 +00:00
|
|
|
if isLeaf:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(DeblobExtGotLeafPrefix)
|
2024-07-02 18:25:06 +00:00
|
|
|
|
|
|
|
var offs = 0
|
|
|
|
VertexRef(
|
2023-05-11 14:25:29 +00:00
|
|
|
vType: Extension,
|
2024-07-02 18:25:06 +00:00
|
|
|
eVid: VertexID(?load64(record, offs, pLen)),
|
2023-05-11 14:25:29 +00:00
|
|
|
ePfx: pathSegment)
|
|
|
|
|
2023-06-12 18:16:03 +00:00
|
|
|
of 3: # `Leaf` vertex
|
2023-05-11 14:25:29 +00:00
|
|
|
let
|
|
|
|
sLen = record[^1].int and 0x3f # length of path segment
|
2024-02-20 03:07:38 +00:00
|
|
|
rLen = record.len - 1 # payload + path segment
|
2023-05-11 14:25:29 +00:00
|
|
|
pLen = rLen - sLen # payload length
|
2024-07-02 18:25:06 +00:00
|
|
|
if rLen < sLen or pLen < 1:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(DeblobLeafSizeGarbled)
|
2024-06-22 20:33:37 +00:00
|
|
|
let (isLeaf, pathSegment) =
|
|
|
|
NibblesBuf.fromHexPrefix record.toOpenArray(pLen, rLen-1)
|
2023-05-11 14:25:29 +00:00
|
|
|
if not isLeaf:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(DeblobLeafGotExtPrefix)
|
2024-07-02 18:25:06 +00:00
|
|
|
let pyl = ? record.toOpenArray(0, pLen - 1).deblobify(PayloadRef)
|
|
|
|
VertexRef(
|
2023-07-05 20:27:48 +00:00
|
|
|
vType: Leaf,
|
|
|
|
lPfx: pathSegment,
|
|
|
|
lData: pyl)
|
2023-09-12 18:45:12 +00:00
|
|
|
|
2023-05-11 14:25:29 +00:00
|
|
|
else:
|
2023-09-12 18:45:12 +00:00
|
|
|
return err(DeblobUnknown)
|
2023-05-11 14:25:29 +00:00
|
|
|
|
2024-06-03 20:10:35 +00:00
|
|
|
proc deblobify*(
|
|
|
|
data: openArray[byte];
|
2024-07-02 18:25:06 +00:00
|
|
|
T: type SavedState;
|
|
|
|
): Result[SavedState,AristoError] =
|
2024-05-31 17:32:22 +00:00
|
|
|
## De-serialise the last saved state data record previously encoded with
|
|
|
|
## `blobify()`.
|
2024-06-28 18:43:04 +00:00
|
|
|
if data.len != 41:
|
2024-05-31 17:32:22 +00:00
|
|
|
return err(DeblobWrongSize)
|
|
|
|
if data[^1] != 0x7f:
|
|
|
|
return err(DeblobWrongType)
|
|
|
|
|
2024-07-02 18:25:06 +00:00
|
|
|
ok(SavedState(
|
|
|
|
key: Hash256(data: array[32, byte].initCopyFrom(data.toOpenArray(0, 31))),
|
|
|
|
serial: uint64.fromBytesBE data.toOpenArray(32, 39)))
|
2024-05-31 17:32:22 +00:00
|
|
|
|
2023-05-11 14:25:29 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# End
|
|
|
|
# ------------------------------------------------------------------------------
|