Skip to content

Commit 085d565

Browse files
authored
MCMS Integration test (#107)
* Integration test - set config * Polish integration test config encoding * Refactor mcms signer key handling * Add setRoot call (no MerkleRoot yet) * Refactor listAsSnake as asSnakeDataUint fn * Add mcms lib structure (merkle-proof init) * Add merkle proof TS fn * Add and use ocr.Signer to fill signatures * Finish first chainOfActions test * Compute root, fix proofLen fn * Post rebase fix * Fix mcms isSignatureValid call, Iterator size (bytes addr), and off-chain signed hash encoding * Fix mcms contract, pass test
1 parent 361de2c commit 085d565

39 files changed

+770
-276
lines changed

contracts/contracts/lib/access/access_control.tolk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ struct AccessControl<T> {
174174
data: AccessControl_Data;
175175

176176
/// Runtime hooks (extensions)
177-
context: T?,
177+
context: T?;
178178
hooks: AccessControl_Hooks<T>?;
179179
}
180180

contracts/contracts/mcms/mcms.tolk

Lines changed: 124 additions & 129 deletions
Large diffs are not rendered by default.

contracts/src/mcms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as merkleProof from './merkle-proof'

contracts/src/mcms/merkle-proof.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { beginCell, Cell } from '@ton/core'
2+
import { asSnakeData, uint8ArrayToBigInt } from '../utils'
3+
4+
import { ocr } from '../../wrappers/libraries/ocr'
5+
import { mcms } from '../../wrappers/mcms'
6+
7+
export const ROOT_METADATA_LEAF_INDEX = 0
8+
9+
export type OpProofs = bigint[][]
10+
11+
export function build(
12+
signers: ocr.Signer[],
13+
validUntil: bigint,
14+
metadata: mcms.RootMetadata,
15+
ops: mcms.Op[],
16+
): [mcms.SetRoot, OpProofs] {
17+
const leaves = constructLeaves(ops, metadata)
18+
19+
const { root, metadataProof, signatures } = constructAnsSignRootAndProof(
20+
leaves,
21+
validUntil,
22+
signers,
23+
)
24+
25+
// Compute proofs for each op
26+
const computeProof = (i: number): bigint[] => computeProofForLeaf(leaves, getLeafIndexOfOp(i))
27+
const opProofs: OpProofs = Array.from({ length: ops.length }, (_, i) => computeProof(i))
28+
29+
const encodeProof = (v) => beginCell().storeUint(v, 256)
30+
const encodeSignature = (v) => mcms.builder.data.signature.encode(v).asBuilder()
31+
32+
return [
33+
{
34+
queryId: 0n,
35+
root,
36+
validUntil,
37+
metadata,
38+
metadataProof: asSnakeData<bigint>(metadataProof, encodeProof),
39+
signatures: asSnakeData<mcms.Signature>(signatures, encodeSignature),
40+
},
41+
opProofs,
42+
]
43+
}
44+
45+
export function computeProofForLeaf(data: bigint[], index: number): bigint[] {
46+
// this method assumes that there is an even number of leaves.
47+
if (data.length % 2 !== 0) {
48+
throw new Error('Invalid proof request: data length must be even')
49+
}
50+
51+
const _proofLen = proofLen(data.length)
52+
53+
const proof: bigint[] = []
54+
while (data.length > 1) {
55+
if ((index & 0x1) === 1) {
56+
proof.push(data[index - 1])
57+
} else {
58+
proof.push(data[index + 1])
59+
}
60+
index = Math.floor(index / 2)
61+
data = hashLevel(data)
62+
}
63+
64+
if (proof.length !== _proofLen) {
65+
throw new Error(`Invalid proof length: expected ${_proofLen}, got ${proof.length}`)
66+
}
67+
68+
return proof
69+
}
70+
71+
export const hashLevel = (data: bigint[]): bigint[] => {
72+
const newData: bigint[] = []
73+
for (let i = 0; i < data.length - 1; i += 2) {
74+
newData.push(hashPair(data[i], data[i + 1]))
75+
}
76+
return newData
77+
}
78+
79+
// Hashes two 256-bit BigInts, ordering them by value before hashing.
80+
export function hashPair(a: bigint, b: bigint): bigint {
81+
return a < b ? hashInternalNode(a, b) : hashInternalNode(b, a)
82+
}
83+
84+
// Hashes an internal Merkle node by concatenating two 256-bit BigInts and hashing.
85+
export function hashInternalNode(left: bigint, right: bigint): bigint {
86+
const data = beginCell().storeUint(left, 256).storeUint(right, 256).endCell()
87+
return uint8ArrayToBigInt(data.hash())
88+
}
89+
90+
export function constructLeaves(ops: mcms.Op[], rootMetadata: mcms.RootMetadata): bigint[] {
91+
const leaves: bigint[] = new Array(ops.length + 1)
92+
93+
// Encode rootMetadata as cell and hash
94+
const leafMetadata = leafMetadataPreimage(rootMetadata).hash()
95+
leaves[ROOT_METADATA_LEAF_INDEX] = uint8ArrayToBigInt(leafMetadata)
96+
97+
for (let i = 0; i < ops.length; i++) {
98+
const leaf = leafOpPreimage(ops[i]).hash()
99+
const leafIndex = i >= ROOT_METADATA_LEAF_INDEX ? i + 1 : i
100+
leaves[leafIndex] = uint8ArrayToBigInt(leaf)
101+
}
102+
103+
return leaves
104+
}
105+
106+
export function leafMetadataPreimage(rootMetadata: mcms.RootMetadata): Cell {
107+
return beginCell()
108+
.storeUint(mcms.MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA, 256)
109+
.storeBuilder(mcms.builder.data.rootMetadata.encode(rootMetadata).asBuilder())
110+
.endCell()
111+
}
112+
113+
export function leafOpPreimage(op: mcms.Op): Cell {
114+
return beginCell()
115+
.storeUint(mcms.MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP, 256)
116+
.storeRef(mcms.builder.data.op.encode(op)) // Doesn't fit in root cell
117+
.endCell()
118+
}
119+
120+
export function proofLen(leavesLen: number): number {
121+
return Math.ceil(Math.log2(leavesLen))
122+
}
123+
124+
export function getLeafIndexOfOp(opIndex: number): number {
125+
return opIndex < ROOT_METADATA_LEAF_INDEX ? opIndex : opIndex + 1
126+
}
127+
128+
export function constructAnsSignRootAndProof(
129+
leaves: bigint[],
130+
validUntil: bigint,
131+
signers: ocr.Signer[],
132+
): {
133+
root: bigint
134+
metadataProof: bigint[]
135+
signatures: mcms.Signature[]
136+
} {
137+
const root = computeRoot(leaves)
138+
const metadataProof = computeProofForLeaf(leaves, ROOT_METADATA_LEAF_INDEX)
139+
const signatures = fillSignatures(root, validUntil, signers)
140+
return { root, metadataProof, signatures }
141+
}
142+
143+
export function computeRoot(leaves: bigint[]): bigint {
144+
let currentLayer = leaves
145+
while (currentLayer.length > 1) {
146+
currentLayer = hashLevel(currentLayer)
147+
}
148+
return currentLayer[0]
149+
}
150+
151+
function fillSignatures(root: bigint, validUntil: bigint, signers: ocr.Signer[]): mcms.Signature[] {
152+
const signatures: mcms.Signature[] = []
153+
const data = beginCell() // TODO: implement as type + CellCodec<T>
154+
.storeUint(root, 256)
155+
.storeUint(validUntil, 32)
156+
.endCell()
157+
.hash()
158+
159+
for (const signer of signers) {
160+
const signature = ocr.createSignatureWith(signer, data)
161+
// TODO: validate signature
162+
signatures.push(signature)
163+
}
164+
165+
return signatures
166+
}

contracts/src/utils/dict.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Dictionary, DictionaryKeyTypes, DictionaryKey, DictionaryValue } from '@ton/core'
2+
3+
export const loadMap = <K extends DictionaryKeyTypes, V>(
4+
key: DictionaryKey<K>,
5+
value: DictionaryValue<V>,
6+
map: Map<K, V>,
7+
): Dictionary<K, V> => {
8+
const dict = Dictionary.empty(key, value)
9+
for (const [k, v] of map) {
10+
dict.set(k, v)
11+
}
12+
return dict
13+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export function fromSnakeData<T>(data: Cell, readerFn: (cs: Slice) => T): T[] {
6666
return array
6767
}
6868

69+
export function asSnakeDataUint(data: bigint[] | number[], bits: number): Cell {
70+
return asSnakeData(data, (item: bigint | number) => new Builder().storeUint(item, bits))
71+
}
72+
6973
export function isEmpty(slice: Slice): boolean {
7074
const remainingBits = slice.remainingBits
7175
const remainingRefs = slice.remainingRefs

contracts/tests/ccip/CCIPRouter.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '../../wrappers/ccip/FeeQuoter'
1212
import { testLog, getExternals } from '../Logs'
1313
import '@ton/test-utils'
14-
import { ZERO_ADDRESS } from '../../utils'
14+
import { ZERO_ADDRESS } from '../../src/utils'
1515

1616
const CHAINSEL_EVM_TEST_90000001 = 909606746561742123n
1717
const CHAINSEL_TON = 13879075125137744094n

contracts/tests/ccip/OffRamp.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
TimestampedPrice,
1212
} from '../../wrappers/ccip/FeeQuoter'
1313
import '@ton/test-utils'
14-
import { uint8ArrayToBigInt, ZERO_ADDRESS } from '../../utils'
14+
import { uint8ArrayToBigInt, ZERO_ADDRESS } from '../../src/utils'
1515
import { KeyPair } from '@ton/crypto'
1616
import { expectEqualsConfig, generateEd25519KeyPair } from '../libraries/ocr/Helpers'
1717
import {

contracts/tests/ccip/helpers/SetUp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'
2-
import { ZERO_ADDRESS } from '../../../utils'
2+
import { ZERO_ADDRESS } from '../../../src/utils'
33
import {
44
createTimestampedPriceValue,
55
FeeQuoter,

0 commit comments

Comments
 (0)