Skip to content

Commit 32ef620

Browse files
committed
Derived workflow owner address utility function
1 parent 6efadaa commit 32ef620

2 files changed

Lines changed: 221 additions & 0 deletions

File tree

pkg/workflows/utils.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"crypto/sha256"
55
"encoding/hex"
66
"strings"
7+
8+
"golang.org/x/crypto/sha3"
79
)
810

911
func EncodeExecutionID(workflowID, eventID string) (string, error) {
@@ -73,6 +75,52 @@ func GenerateWorkflowID(owner []byte, name string, workflow []byte, config []byt
7375
return sha, nil
7476
}
7577

78+
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+
86+
// Write 0xff byte
87+
_, err := outerHash.Write([]byte{0xff})
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
// Write 84 zero bytes because preimage for the final hashing round is always exactly 85 bytes
93+
zeroBytes := make([]byte, 84)
94+
_, err = outerHash.Write(zeroBytes)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
// Creation of the nested hash
100+
nestedHash := sha3.NewLegacyKeccak256()
101+
102+
// Write prefix
103+
_, err = nestedHash.Write([]byte(prefix))
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
// Write ownerKey
109+
_, err = nestedHash.Write([]byte(ownerKey))
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
// Write the nested hash within the outer hash
115+
_, err = outerHash.Write(nestedHash.Sum(nil))
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
// Return the last 20 bytes (EVM compatible address)
121+
return outerHash.Sum(nil)[12:], nil
122+
}
123+
76124
// HashTruncateName returns the SHA-256 hash of the workflow name truncated to the first 10 bytes.
77125
func HashTruncateName(name string) string {
78126
// Compute SHA-256 hash of the input string

pkg/workflows/utils_test.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package workflows
33
import (
44
"crypto/sha256"
55
"encoding/hex"
6+
"strings"
67
"testing"
78
"unicode/utf8"
89

@@ -108,3 +109,175 @@ func TestNormalizeWorkflowName(t *testing.T) {
108109
})
109110
}
110111
}
112+
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
120+
description string
121+
}{
122+
{
123+
name: "different_prefix",
124+
prefix1: "registry_v1",
125+
ownerKey1: "owner123",
126+
prefix2: "registry_v2",
127+
ownerKey2: "owner123",
128+
description: "Same ownerKey, different prefix",
129+
},
130+
{
131+
name: "different_ownerKey",
132+
prefix1: "registry_v1",
133+
ownerKey1: "owner123",
134+
prefix2: "registry_v1",
135+
ownerKey2: "owner124",
136+
description: "Same prefix, different ownerKey",
137+
},
138+
{
139+
name: "case_sensitive_prefix",
140+
prefix1: "Registry",
141+
ownerKey1: "owner123",
142+
prefix2: "registry",
143+
ownerKey2: "owner123",
144+
description: "Case sensitive prefix difference",
145+
},
146+
{
147+
name: "case_sensitive_ownerKey",
148+
prefix1: "registry",
149+
ownerKey1: "Owner123",
150+
prefix2: "registry",
151+
ownerKey2: "owner123",
152+
description: "Case sensitive ownerKey difference",
153+
},
154+
{
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",
161+
},
162+
{
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",
169+
},
170+
}
171+
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")
177+
178+
// Generate second address
179+
addr2, err := GenerateWorkflowOwnerAddress(tt.prefix2, tt.ownerKey2)
180+
require.NoError(t, err, "Failed to generate second address")
181+
182+
// Verify addresses are different
183+
require.NotEqual(t, hex.EncodeToString(addr1), hex.EncodeToString(addr2), "Addresses should not match")
184+
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")
188+
})
189+
}
190+
}
191+
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
246+
testCases := []struct {
247+
prefix string
248+
ownerKey string
249+
expectedHex string // This should be generated by running the Solidity contract
250+
}{
251+
{
252+
prefix: "registry1",
253+
ownerKey: "owner123",
254+
expectedHex: "0x58c0e4aaf5fb13fcaea5790f8a19014ad9646da3", // convert to lowercase, not checksum
255+
},
256+
{
257+
prefix: "registry2",
258+
ownerKey: "owner123",
259+
expectedHex: "0xf094995741cffc6c173fa9edb2e8d766d1524039", // convert to lowercase, not checksum
260+
},
261+
{
262+
prefix: "registry2",
263+
ownerKey: "ownerSomethingElse",
264+
expectedHex: "0x4be6a8e38aa493cac0aa4c6dd13bad41f8219f0c", // convert to lowercase, not checksum
265+
},
266+
}
267+
268+
for _, tc := range testCases {
269+
t.Run(tc.prefix+"_"+tc.ownerKey, func(t *testing.T) {
270+
goAddr, err := GenerateWorkflowOwnerAddress(tc.prefix, tc.ownerKey)
271+
require.NoError(t, err)
272+
273+
goAddrHex := hex.EncodeToString(goAddr)
274+
275+
// Remove 0x prefix if present in expected
276+
expected := strings.TrimPrefix(tc.expectedHex, "0x")
277+
278+
require.Equal(t, expected, goAddrHex,
279+
"Go implementation should match Solidity for prefix='%s', ownerKey='%s'",
280+
tc.prefix, tc.ownerKey)
281+
})
282+
}
283+
}

0 commit comments

Comments
 (0)