A block is JavaScript, and register_block_type is the PHP half

A block exists in two places: the editor knows about it through a JavaScript registration, and the server knows about it through a PHP one — and both are required for anything dynamic.

function turkerdev_register_notice_block() {
    wp_register_script(
        'turkerdev-notice',
        plugins_url( 'build/notice.js', __FILE__ ),
        array( 'wp-blocks', 'wp-element', 'wp-editor' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/notice.js' )
    );

    register_block_type( 'turkerdev/notice', array(
        'editor_script'   => 'turkerdev-notice',
        'render_callback' => 'turkerdev_render_notice',
    ) );
}
add_action( 'init', 'turkerdev_register_notice_block' );

Registering on init rather than an admin hook matters because the render callback has to exist on the front end too. filemtime as the version is a small thing that saves a lot of cache confusion during development. The dependency array is what pulls the editor packages in as externals rather than bundling React twice, which is the single most common cause of a plugin that adds four hundred kilobytes to the admin.