Skip to content

Commit cac6685

Browse files
committed
Update to the derivation function and more test vectors
1 parent 32ef620 commit cac6685

File tree

2 files changed

+151
-148
lines changed

2 files changed

+151
-148
lines changed

pkg/workflows/utils.go

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package workflows
22

33
import (
44
"crypto/sha256"
5+
"encoding/binary"
56
"encoding/hex"
67
"strings"
78

@@ -75,50 +76,63 @@ func GenerateWorkflowID(owner []byte, name string, workflow []byte, config []byt
7576
return sha, nil
7677
}
7778

79+
// CREATE2-style address derivation with domain separation and collision resistance:
80+
// ownerAddress = keccak256(0xff ++ bytes.repeat(0x0, 84) ++
81+
// "Chainlink Runtime Environment GenerateWorkflowOwnerAddress\x00" ++
82+
// len(prefix).to_bytes(8, byteorder='big') ++ prefix ++
83+
// len(ownerKey).to_bytes(8, byteorder='big') ++ ownerKey)[12:]
7884
func GenerateWorkflowOwnerAddress(prefix string, ownerKey string) ([]byte, error) {
79-
// CREATE2 proposed in EIP-1014:
80-
// keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]
81-
// CREATE2-style address derivation inspired by the above:
82-
// ownerAddress = keccak256(0xff ++ bytes.repeat(0x0, 84) ++ keccak256(prefix ++ ownerKey))[12:]
83-
84-
outerHash := sha3.NewLegacyKeccak256()
85+
hash := sha3.NewLegacyKeccak256()
8586

8687
// Write 0xff byte
87-
_, err := outerHash.Write([]byte{0xff})
88+
_, err := hash.Write([]byte{0xff})
8889
if err != nil {
8990
return nil, err
9091
}
9192

9293
// Write 84 zero bytes because preimage for the final hashing round is always exactly 85 bytes
9394
zeroBytes := make([]byte, 84)
94-
_, err = outerHash.Write(zeroBytes)
95+
_, err = hash.Write(zeroBytes)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
// Write domain separator string
101+
domainSeparator := "Chainlink Runtime Environment GenerateWorkflowOwnerAddress\x00"
102+
_, err = hash.Write([]byte(domainSeparator))
95103
if err != nil {
96104
return nil, err
97105
}
98106

99-
// Creation of the nested hash
100-
nestedHash := sha3.NewLegacyKeccak256()
107+
// Write length-prefixed prefix as big endian
108+
prefixLen := uint64(len(prefix))
109+
err = binary.Write(hash, binary.BigEndian, prefixLen)
110+
if err != nil {
111+
return nil, err
112+
}
101113

102114
// Write prefix
103-
_, err = nestedHash.Write([]byte(prefix))
115+
_, err = hash.Write([]byte(prefix))
104116
if err != nil {
105117
return nil, err
106118
}
107119

108-
// Write ownerKey
109-
_, err = nestedHash.Write([]byte(ownerKey))
120+
// Write length-prefixed ownerKey as big endian
121+
ownerKeyLen := uint64(len(ownerKey))
122+
err = binary.Write(hash, binary.BigEndian, ownerKeyLen)
110123
if err != nil {
111124
return nil, err
112125
}
113126

114-
// Write the nested hash within the outer hash
115-
_, err = outerHash.Write(nestedHash.Sum(nil))
127+
// Write ownerKey
128+
_, err = hash.Write([]byte(ownerKey))
116129
if err != nil {
117130
return nil, err
118131
}
119132

120-
// Return the last 20 bytes (EVM compatible address)
121-
return outerHash.Sum(nil)[12:], nil
133+
// Return the first 20 bytes (Ethereum address)
134+
fullHash := hash.Sum(nil)
135+
return fullHash[:20], nil
122136
}
123137

124138
// HashTruncateName returns the SHA-256 hash of the workflow name truncated to the first 10 bytes.

pkg/workflows/utils_test.go

Lines changed: 120 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -110,174 +110,163 @@ func TestNormalizeWorkflowName(t *testing.T) {
110110
}
111111
}
112112

113-
func Test_GenerateWorkflowOwnerAddress_DifferentInputsGenerateDifferentAddresses(t *testing.T) {
114-
tests := []struct {
115-
name string
116-
prefix1 string
117-
ownerKey1 string
118-
prefix2 string
119-
ownerKey2 string
113+
func Test_GenerateWorkflowOwnerAddress_SameInputsGenerateSameAddress(t *testing.T) {
114+
prefix := "test_registry"
115+
ownerKey := "test_owner_123"
116+
117+
// Generate address multiple times
118+
addr1, err := GenerateWorkflowOwnerAddress(prefix, ownerKey)
119+
require.NoError(t, err, "Failed to generate first address")
120+
121+
addr2, err := GenerateWorkflowOwnerAddress(prefix, ownerKey)
122+
require.NoError(t, err, "Failed to generate second address")
123+
124+
// Verify all addresses are identical
125+
addr1Hex := hex.EncodeToString(addr1)
126+
addr2Hex := hex.EncodeToString(addr2)
127+
128+
require.Equal(t, addr1Hex, addr2Hex, "Same inputs should generate same address")
129+
}
130+
131+
func Test_GenerateWorkflowOwnerAddress_OtherTestVectors(t *testing.T) {
132+
testVectors := []struct {
133+
prefix string
134+
ownerKey string
135+
keccakInput string
136+
keccakHash string
137+
ethAddress string
120138
description string
121139
}{
122140
{
123-
name: "different_prefix",
124-
prefix1: "registry_v1",
125-
ownerKey1: "owner123",
126-
prefix2: "registry_v2",
127-
ownerKey2: "owner123",
128-
description: "Same ownerKey, different prefix",
141+
prefix: "",
142+
ownerKey: "",
143+
ethAddress: "0x41305062a5e522A01B7D9460E6744C879113C5dB",
144+
description: "Empty prefix and ownerKey",
145+
},
146+
{
147+
prefix: "",
148+
ownerKey: "non_empty_owner_key",
149+
ethAddress: "0xa6b6C2C0D58bD45c83b89F60cae395F7f3c9A0D0",
150+
description: "Empty prefix, non-empty ownerKey",
151+
},
152+
{
153+
prefix: "non_empty_prefix",
154+
ownerKey: "",
155+
ethAddress: "0x8Cb3243107cAD0D5584cD8b37393FE7f5200B920",
156+
description: "Non-empty prefix, empty ownerKey",
157+
},
158+
{
159+
prefix: "x",
160+
ownerKey: "yz",
161+
ethAddress: "0xb2a39e39664A469bc1d1b5dB8592deda4E9410af",
162+
description: "Collision test case 1: x + yz",
129163
},
130164
{
131-
name: "different_ownerKey",
132-
prefix1: "registry_v1",
133-
ownerKey1: "owner123",
134-
prefix2: "registry_v1",
135-
ownerKey2: "owner124",
136-
description: "Same prefix, different ownerKey",
165+
prefix: "xy",
166+
ownerKey: "z",
167+
ethAddress: "0x561960c471b8B288284073457EB77175C49DA9cd",
168+
description: "Collision test case 2: xy + z (should be different from x + yz)",
137169
},
138170
{
139-
name: "case_sensitive_prefix",
140-
prefix1: "Registry",
141-
ownerKey1: "owner123",
142-
prefix2: "registry",
143-
ownerKey2: "owner123",
144-
description: "Case sensitive prefix difference",
171+
prefix: "org_123456789",
172+
ownerKey: "cre-storage-service",
173+
ethAddress: "0x95b028290D5E2aC912f0bA8e9E35931B90740608",
174+
description: "Realistic org ID and service name",
145175
},
146176
{
147-
name: "case_sensitive_ownerKey",
148-
prefix1: "registry",
149-
ownerKey1: "Owner123",
150-
prefix2: "registry",
151-
ownerKey2: "owner123",
152-
description: "Case sensitive ownerKey difference",
177+
prefix: "org_0x1234567890abcdef",
178+
ownerKey: "cre-storage-service",
179+
ethAddress: "0x102d1d8570F155D4C9AF463C7098B978fEaEDf1C",
180+
description: "Hex-prefixed org ID",
153181
},
154182
{
155-
name: "single_char_difference_prefix",
156-
prefix1: "registrya",
157-
ownerKey1: "owner123",
158-
prefix2: "registryb",
159-
ownerKey2: "owner123",
160-
description: "Single character difference in prefix",
183+
prefix: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
184+
ownerKey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
185+
ethAddress: "0x31B1AfA30824ab78fF1c3f7b26ef6ce50D9c3221",
186+
description: "Long repeating characters",
161187
},
162188
{
163-
name: "single_char_difference_ownerKey",
164-
prefix1: "registry",
165-
ownerKey1: "owner123a",
166-
prefix2: "registry",
167-
ownerKey2: "owner123b",
168-
description: "Single character difference in ownerKey",
189+
prefix: "org_public_key",
190+
ownerKey: "0x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
191+
ethAddress: "0x9Ed0D44aB4B2FCC1B4C280105D641F668b81A6e9",
192+
description: "Public key as ownerKey",
193+
},
194+
{
195+
prefix: "org@special#chars",
196+
ownerKey: "key$with%symbols",
197+
ethAddress: "0xAbF538FFffFAE8C139c0BF80e7cEC5c1596D0944",
198+
description: "Special characters in both prefix and ownerKey",
169199
},
170200
}
171201

172-
for _, tt := range tests {
173-
t.Run(tt.name, func(t *testing.T) {
174-
// Generate first address
175-
addr1, err := GenerateWorkflowOwnerAddress(tt.prefix1, tt.ownerKey1)
176-
require.NoError(t, err, "Failed to generate first address")
202+
for _, tv := range testVectors {
203+
t.Run(tv.description, func(t *testing.T) {
204+
addr, err := GenerateWorkflowOwnerAddress(tv.prefix, tv.ownerKey)
205+
require.NoError(t, err, "Failed to generate address")
177206

178-
// Generate second address
179-
addr2, err := GenerateWorkflowOwnerAddress(tt.prefix2, tt.ownerKey2)
180-
require.NoError(t, err, "Failed to generate second address")
207+
expectedAddr := tv.ethAddress
208+
if expectedAddr[:2] == "0x" {
209+
expectedAddr = expectedAddr[2:]
210+
}
211+
expectedAddr = strings.ToLower(expectedAddr)
212+
actualAddr := hex.EncodeToString(addr)
181213

182-
// Verify addresses are different
183-
require.NotEqual(t, hex.EncodeToString(addr1), hex.EncodeToString(addr2), "Addresses should not match")
214+
require.Equal(t, expectedAddr, actualAddr, "Address mismatch")
184215

185-
// Verify addresses are 20 bytes (Ethereum address length)
186-
require.Len(t, addr1, 20, "First address should be 20 bytes")
187-
require.Len(t, addr2, 20, "Second address should be 20 bytes")
216+
require.Len(t, addr, 20, "Address should be 20 bytes")
188217
})
189218
}
190219
}
191220

192-
func Test_GenerateWorkflowOwnerAddress_SameInputsGenerateSameAddress(t *testing.T) {
193-
prefix := "test_registry"
194-
ownerKey := "test_owner_123"
195-
196-
// Generate address multiple times
197-
addr1, err := GenerateWorkflowOwnerAddress(prefix, ownerKey)
198-
require.NoError(t, err, "Failed to generate first address")
199-
200-
addr2, err := GenerateWorkflowOwnerAddress(prefix, ownerKey)
201-
require.NoError(t, err, "Failed to generate second address")
202-
203-
// Verify all addresses are identical
204-
addr1Hex := hex.EncodeToString(addr1)
205-
addr2Hex := hex.EncodeToString(addr2)
206-
207-
require.Equal(t, addr1Hex, addr2Hex, "Same inputs should generate same address")
208-
}
209-
210-
func Test_GenerateWorkflowOwnerAddress_SolidityCompatibility(t *testing.T) {
211-
/*
212-
// SPDX-License-Identifier: MIT
213-
pragma solidity ^0.8.0;
214-
215-
contract WorkflowOwnerAddressGenerator {
216-
217-
function generateWorkflowOwnerAddress(
218-
string memory prefix,
219-
string memory ownerKey
220-
) public pure returns (address) {
221-
// Step 1: Create nested hash of prefix + ownerKey
222-
bytes32 nestedHash = keccak256(abi.encodePacked(prefix, ownerKey));
223-
224-
// Step 2: Create the full preimage for outer hash
225-
// 0xff + 84 zero bytes + nested hash
226-
bytes memory preimage = new bytes(117); // 1 + 84 + 32 = 117 bytes
227-
228-
// Set first byte to 0xff
229-
preimage[0] = 0xff;
230-
231-
// Bytes 1-84 are already zero (default in Solidity)
232-
233-
// Copy nested hash to bytes 85-116
234-
for (uint256 i = 0; i < 32; i++) {
235-
preimage[85 + i] = nestedHash[i];
236-
}
237-
238-
// Step 3: Hash the full preimage and return last 20 bytes as address
239-
bytes32 outerHash = keccak256(preimage);
240-
return address(uint160(uint256(outerHash)));
241-
}
242-
}
243-
*/
244-
// These expected addresses were generated using the Solidity contract above
245-
// You can verify these by deploying the contract and calling the function
221+
func Test_GenerateWorkflowOwnerAddress_CollisionResistanceWithLengthPrefixing(t *testing.T) {
222+
// Test that the length-prefixing prevents collisions
246223
testCases := []struct {
247-
prefix string
248-
ownerKey string
249-
expectedHex string // This should be generated by running the Solidity contract
224+
prefix1 string
225+
owner1 string
226+
prefix2 string
227+
owner2 string
228+
caseName string
250229
}{
251230
{
252-
prefix: "registry1",
253-
ownerKey: "owner123",
254-
expectedHex: "0x58c0e4aaf5fb13fcaea5790f8a19014ad9646da3", // convert to lowercase, not checksum
231+
prefix1: "x",
232+
owner1: "yz",
233+
prefix2: "xy",
234+
owner2: "z",
235+
caseName: "x+yz vs xy+z collision resistance",
236+
},
237+
{
238+
prefix1: "a",
239+
owner1: "bc",
240+
prefix2: "ab",
241+
owner2: "c",
242+
caseName: "a+bc vs ab+c collision resistance",
255243
},
256244
{
257-
prefix: "registry2",
258-
ownerKey: "owner123",
259-
expectedHex: "0xf094995741cffc6c173fa9edb2e8d766d1524039", // convert to lowercase, not checksum
245+
prefix1: "",
246+
owner1: "abc",
247+
prefix2: "a",
248+
owner2: "bc",
249+
caseName: "empty+abc vs a+bc collision resistance",
260250
},
261251
{
262-
prefix: "registry2",
263-
ownerKey: "ownerSomethingElse",
264-
expectedHex: "0x4be6a8e38aa493cac0aa4c6dd13bad41f8219f0c", // convert to lowercase, not checksum
252+
prefix1: "test",
253+
owner1: "",
254+
prefix2: "tes",
255+
owner2: "t",
256+
caseName: "test+empty vs tes+t collision resistance",
265257
},
266258
}
267259

268260
for _, tc := range testCases {
269-
t.Run(tc.prefix+"_"+tc.ownerKey, func(t *testing.T) {
270-
goAddr, err := GenerateWorkflowOwnerAddress(tc.prefix, tc.ownerKey)
261+
t.Run(tc.caseName, func(t *testing.T) {
262+
addr1, err := GenerateWorkflowOwnerAddress(tc.prefix1, tc.owner1)
271263
require.NoError(t, err)
272264

273-
goAddrHex := hex.EncodeToString(goAddr)
274-
275-
// Remove 0x prefix if present in expected
276-
expected := strings.TrimPrefix(tc.expectedHex, "0x")
265+
addr2, err := GenerateWorkflowOwnerAddress(tc.prefix2, tc.owner2)
266+
require.NoError(t, err)
277267

278-
require.Equal(t, expected, goAddrHex,
279-
"Go implementation should match Solidity for prefix='%s', ownerKey='%s'",
280-
tc.prefix, tc.ownerKey)
268+
require.NotEqual(t, hex.EncodeToString(addr1), hex.EncodeToString(addr2),
269+
"Addresses should be different")
281270
})
282271
}
283272
}

0 commit comments

Comments
 (0)